From 4eeec1865f845fd7a0c07875e726b656c0a3278a Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 21 Apr 2023 15:45:55 -0500 Subject: [PATCH] [Security Solution] add threat intelligence overview to expandable flyout (#155328) --- ...ert_details_right_panel_overview_tab.cy.ts | 36 +++ .../screens/document_expandable_flyout.ts | 12 + .../right/components/insights_section.tsx | 2 + .../insights_subsection.stories.tsx | 38 +++ .../components/insights_subsection.test.tsx | 67 ++++++ .../right/components/insights_subsection.tsx | 79 +++++++ .../insights_summary_panel.stories.tsx | 156 +++++++++++++ .../insights_summary_panel.test.tsx | 90 ++++++++ .../components/insights_summary_panel.tsx | 106 +++++++++ .../flyout/right/components/test_ids.ts | 12 + .../threat_intelligence_overview.test.tsx | 195 ++++++++++++++++ .../threat_intelligence_overview.tsx | 91 ++++++++ .../flyout/right/components/translations.ts | 42 ++++ .../use_fetch_threat_intelligence.test.tsx | 216 ++++++++++++++++++ .../hooks/use_fetch_threat_intelligence.ts | 108 +++++++++ 15 files changed, 1250 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index c98c84b800079..57ea1a2d4efdb 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -29,6 +29,10 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON, DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON, } from '../../../screens/document_expandable_flyout'; import { expandFirstAlertExpandableFlyout, @@ -180,6 +184,38 @@ describe.skip( .click(); cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + it.skip('should display threat intelligence section', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) + .scrollIntoView() + .should('be.visible') + .and('have.text', 'Threat Intelligence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT) + .should('be.visible') + .within(() => { + // threat match detected + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(0) + .should('be.visible') + .and('have.text', '1 threat match detected'); // TODO + + // field with threat enrichement + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(1) + .should('be.visible') + .and('have.text', '1 field enriched with threat intelligence'); // TODO + }); + }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + // and improve when we can navigate Threat Intelligence to sub tab directly + it.skip('should navigate to left panel, entities tab when view all fields of threat intelligence is clicked', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON) + .should('be.visible') + .click(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); + }); }); describe('visualizations section', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index 5645a7de9f6ac..dafd16932d194 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -73,6 +73,10 @@ import { ENTITIES_VIEW_ALL_BUTTON_TEST_ID, VISUALIZATIONS_SECTION_HEADER_TEST_ID, ANALYZER_TREE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, } from '../../public/flyout/right/components/test_ids'; import { getClassSelector, @@ -303,6 +307,14 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER = getDataTestSubjectSelector(ENTITY_PANEL_HEADER_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT = getDataTestSubjectSelector(ENTITY_PANEL_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER = getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx index 8409676b610b0..9efb979ba7a26 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { INSIGHTS_TEST_ID } from './test_ids'; import { INSIGHTS_TITLE } from './translations'; import { EntitiesOverview } from './entities_overview'; @@ -25,6 +26,7 @@ export const InsightsSection: React.FC = ({ expanded = fal return ( + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx new file mode 100644 index 0000000000000..c1fc9dfe8a7f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx @@ -0,0 +1,38 @@ +/* + * 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 { InsightsSubSection } from './insights_subsection'; + +export default { + component: InsightsSubSection, + title: 'Flyout/InsightsSubSection', +}; + +const title = 'Title'; +const children =
{'hello'}
; + +export const Basic: Story = () => { + return {children}; +}; + +export const Loading: Story = () => { + return ( + + {null} + + ); +}; + +export const NoTitle: Story = () => { + return {children}; +}; + +export const NoChildren: Story = () => { + return {null}; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx new file mode 100644 index 0000000000000..271953c8e8105 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { InsightsSubSection } from './insights_subsection'; + +const title = 'Title'; +const dataTestSubj = 'test'; +const children =
{'hello'}
; + +describe('', () => { + it('should render children component', () => { + const { getByTestId } = render( + + {children} + + ); + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + expect(getByTestId(titleDataTestSubj)).toHaveTextContent(title); + expect(getByTestId(contentDataTestSubj)).toBeInTheDocument(); + }); + + it('should render loading component', () => { + const { getByTestId } = render( + + {children} + + ); + + const loadingDataTestSubj = `${dataTestSubj}Loading`; + expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument(); + }); + + it('should render null if error', () => { + const { container } = render( + + {children} + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no title', () => { + const { container } = render({children}); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no children', () => { + const { container } = render( + + {null} + + ); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx new file mode 100644 index 0000000000000..4b5c1a541e316 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; + +export interface InsightsSectionProps { + /** + * Renders a loading spinner if true + */ + loading?: boolean; + /** + * Returns a null component if true + */ + error?: boolean; + /** + * Title at the top of the component + */ + title: string; + /** + * Content of the component + */ + children: React.ReactNode; + /** + * Prefix data-test-subj to use for the elements + */ + ['data-test-subj']?: string; +} + +/** + * Presentational component to handle loading and error in the subsections of the Insights section. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results + */ +export const InsightsSubSection: React.FC = ({ + loading = false, + error = false, + title, + 'data-test-subj': dataTestSubj, + children, +}) => { + const loadingDataTestSubj = `${dataTestSubj}Loading`; + // showing the loading in this component instead of SummaryPanel because we're hiding the entire section if no data + + if (loading) { + return ( + + + + + + ); + } + + // hide everything + if (error || !title || !children) { + return null; + } + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + return ( + <> + +
{title}
+
+ + + {children} + + + ); +}; + +InsightsSubSection.displayName = 'InsightsSubSection'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx new file mode 100644 index 0000000000000..5637d3c036860 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx @@ -0,0 +1,156 @@ +/* + * 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 { css } from '@emotion/react'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +export default { + component: InsightsSummaryPanel, + title: 'Flyout/InsightsSummaryPanel', +}; + +export const Default: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + color: 'rgb(255,126,98)', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + color: 'rgb(241,216,11)', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const InvalidColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for an invalid color (abc)', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const LongText: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is an extremely long text to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; +export const LongNumber: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is an extremely long value to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoData: Story = () => { + const data: InsightsSummaryPanelData[] = []; + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx new file mode 100644 index 0000000000000..9ecbbcc7fc0a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { + INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, +} from './test_ids'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +describe('', () => { + it('should render by default', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const iconTestId = `${INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID}0`; + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + const colorTestId = `${INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID}0`; + expect(getByTestId(iconTestId)).toBeInTheDocument(); + expect(getByTestId(valueTestId)).toHaveTextContent('1 this is a test for red'); + expect(getByTestId(colorTestId)).toBeInTheDocument(); + }); + + it('should only render null when data is null', () => { + const data = null as unknown as InsightsSummaryPanelData[]; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should handle big number in a compact notation', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + expect(getByTestId(valueTestId)).toHaveTextContent('160k this is a test for red'); + }); + + it(`should not show the colored dot if color isn't provided`, () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for no color', + }, + ]; + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx new file mode 100644 index 0000000000000..306eaa101b804 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx @@ -0,0 +1,106 @@ +/* + * 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 from 'react'; +import { css } from '@emotion/react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHealth, EuiPanel } from '@elastic/eui'; +import { FormattedCount } from '../../../common/components/formatted_number'; + +export interface InsightsSummaryPanelData { + /** + * Icon to display on the left side of each row + */ + icon: string; + /** + * Number of results/entries found + */ + value: number; + /** + * Text corresponding of the number of results/entries + */ + text: string; + /** + * Optional parameter for now, will be used to display a dot on the right side + * (corresponding to some sort of severity?) + */ + color?: string; // TODO remove optional when we have guidance on what the colors will actually be +} + +export interface InsightsSummaryPanelProps { + /** + * Array of data to display in each row + */ + data: InsightsSummaryPanelData[]; + /** + * Prefix data-test-subj because this component will be used in multiple places + */ + ['data-test-subj']?: string; +} + +/** + * Panel showing summary information as an icon, a count and text as well as a severity colored dot. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results components under the Insights section. + * The colored dot is currently optional but will ultimately be mandatory (waiting on PM and UIUX). + */ +export const InsightsSummaryPanel: VFC = ({ + data, + 'data-test-subj': dataTestSubj, +}) => { + if (!data || data.length === 0) { + return null; + } + + const iconDataTestSubj = `${dataTestSubj}Icon`; + const valueDataTestSubj = `${dataTestSubj}Value`; + const colorDataTestSubj = `${dataTestSubj}Color`; + + return ( + + + {data.map((row, index) => ( + + + + + + {row.text} + + {row.color && ( + + + + )} + + ))} + + + ); +}; + +InsightsSummaryPanel.displayName = 'InsightsSummaryPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index e89c16b3f5f12..9ee38ba2940cb 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -84,6 +84,18 @@ export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewRiskLevel'; +/* Insights Threat Intelligence */ + +export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence'; +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Title`; +export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Content`; +export const INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`; +export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Loading`; +export const INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Icon`; +export const INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Value`; +export const INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Color`; + /* Visualizations section*/ export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle'; export const VISUALIZATIONS_SECTION_HEADER_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx new file mode 100644 index 0000000000000..ecf17f2c7e822 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx @@ -0,0 +1,195 @@ +/* + * 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 { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { RightPanelContext } from '../context'; +import { + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, +} from './test_ids'; +import { TestProviders } from '../../../common/mock'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; + +jest.mock('../hooks/use_fetch_threat_intelligence'); + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + dataFormattedForFieldBrowser: [], +} as unknown as RightPanelContext; + +const renderThreatIntelligenceOverview = (contextValue: RightPanelContext) => ( + + + + + +); + +describe('', () => { + it('should render 1 match detected and 1 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 threat match detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 field enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 2, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 threat matches detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 fields enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 0 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 0, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 field enriched with threat intelligence' + ); + }); + + it('should render 0 match detected', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 threat match detected' + ); + }); + + it('should render loading', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: true, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID)).toBeInTheDocument(); + }); + + it('should render null when eventId is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + }); + const contextValue = { + ...panelContextValue, + eventId: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when dataFormattedForFieldBrowser is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + error: true, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when no enrichment found is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 0, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should navigate to left section Insights tab when clicking on button', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + const flyoutContextValue = { + openLeftPanel: jest.fn(), + } as unknown as ExpandableFlyoutContext; + + const { getByTestId } = render( + + + + + + + + ); + + getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx new file mode 100644 index 0000000000000..63f0862a68b3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx @@ -0,0 +1,91 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; +import { InsightsSubSection } from './insights_subsection'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; +import { useRightPanelContext } from '../context'; +import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; +import { + VIEW_ALL, + THREAT_INTELLIGENCE_TITLE, + THREAT_INTELLIGENCE_TEXT, + THREAT_MATCH_DETECTED, + THREAT_ENRICHMENT, + THREAT_MATCHES_DETECTED, + THREAT_ENRICHMENTS, +} from './translations'; +import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; + +/** + * Threat Intelligence section under Insights section, overview tab. + * The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state, + * and the SummaryPanel component for data rendering. + */ +export const ThreatIntelligenceOverview: React.FC = () => { + const { eventId, indexName, dataFormattedForFieldBrowser } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + + const goToThreatIntelligenceTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: eventId, + indexName, + }, + }); + }, [eventId, openLeftPanel, indexName]); + + const { loading, threatMatchesCount, threatEnrichmentsCount } = useFetchThreatIntelligence({ + dataFormattedForFieldBrowser, + }); + + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: threatMatchesCount, + text: threatMatchesCount <= 1 ? THREAT_MATCH_DETECTED : THREAT_MATCHES_DETECTED, + }, + { + icon: 'warning', + value: threatEnrichmentsCount, + text: threatMatchesCount <= 1 ? THREAT_ENRICHMENT : THREAT_ENRICHMENTS, + }, + ]; + + const error: boolean = + !eventId || + !dataFormattedForFieldBrowser || + (threatMatchesCount === 0 && threatEnrichmentsCount === 0); + + return ( + + + + {VIEW_ALL(THREAT_INTELLIGENCE_TEXT)} + + + ); +}; + +ThreatIntelligenceOverview.displayName = 'ThreatIntelligenceOverview'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 1a6a2da7344c4..d5b9f0d1928b3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -101,11 +101,18 @@ export const HIGHLIGHTED_FIELDS_TITLE = i18n.translate( { defaultMessage: 'Highlighted fields' } ); +/* Insights section */ + export const ENTITIES_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.entitiesTitle', { defaultMessage: 'Entities' } ); +export const THREAT_INTELLIGENCE_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle', + { defaultMessage: 'Threat Intelligence' } +); + export const INSIGHTS_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.insightsTitle', { defaultMessage: 'Insights' } @@ -131,6 +138,41 @@ export const ENTITIES_TEXT = i18n.translate( } ); +export const THREAT_INTELLIGENCE_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText', + { + defaultMessage: 'fields of threat intelligence', + } +); + +export const THREAT_MATCH_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch', + { + defaultMessage: `threat match detected`, + } +); + +export const THREAT_MATCHES_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches', + { + defaultMessage: `threat matches detected`, + } +); + +export const THREAT_ENRICHMENT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment', + { + defaultMessage: `field enriched with threat intelligence`, + } +); + +export const THREAT_ENRICHMENTS = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments', + { + defaultMessage: `fields enriched with threat intelligence`, + } +); + export const VIEW_ALL = (text: string) => i18n.translate('xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton', { values: { text }, diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx new file mode 100644 index 0000000000000..ab57dcebe32af --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx @@ -0,0 +1,216 @@ +/* + * 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 { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { + UseThreatIntelligenceParams, + UseThreatIntelligenceValue, +} from './use_fetch_threat_intelligence'; +import { useFetchThreatIntelligence } from './use_fetch_threat_intelligence'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; + +jest.mock('../../../common/containers/cti/event_enrichment'); + +const dataFormattedForFieldBrowser = [ + { + category: 'kibana', + field: 'kibana.alert.rule.uuid', + isObjectArray: false, + originalValue: ['uuid'], + values: ['uuid'], + }, + { + category: 'threat', + field: 'threat.enrichments', + isObjectArray: true, + originalValue: ['{"indicator.file.hash.sha256":["sha256"]}'], + values: ['{"indicator.file.hash.sha256":["sha256"]}'], + }, + { + category: 'threat', + field: 'threat.enrichments.indicator.file.hash.sha256', + isObjectArray: false, + originalValue: ['sha256'], + values: ['sha256'], + }, +]; + +describe('useFetchThreatIntelligence', () => { + let hookResult: RenderHookResult; + + it('should render 1 match detected and 1 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 2, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.3'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.4'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 4, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(2); + expect(hookResult.result.current.threatMatchesCount).toEqual(2); + expect(hookResult.result.current.threatEnrichments).toHaveLength(2); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(2); + }); + + it('should render 0 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should render 0 match detected', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should return loading true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: undefined, + loading: true, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should return error true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [], + totalCount: 0, + }, + loading: false, + }); + + hookResult = renderHook(() => + useFetchThreatIntelligence({ dataFormattedForFieldBrowser: null }) + ); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts new file mode 100644 index 0000000000000..4f3d23b082664 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts @@ -0,0 +1,108 @@ +/* + * 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 { useMemo } from 'react'; +import { groupBy } from 'lodash'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import type { CtiEnrichment } from '../../../../common/search_strategy'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, + timelineDataToEnrichment, +} from '../../../common/components/event_details/cti_details/helpers'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; +import { ENRICHMENT_TYPES } from '../../../../common/cti/constants'; + +export interface UseThreatIntelligenceParams { + /** + * An array of field objects with category and value + */ + dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null; +} + +export interface UseThreatIntelligenceValue { + /** + * Returns true while the threat intelligence data is being queried + */ + loading: boolean; + /** + * Returns true if the dataFormattedForFieldBrowser property is null + */ + error: boolean; + /** + * Threat matches (from an indicator match rule) + */ + threatMatches: CtiEnrichment[]; + /** + * Threat matches count + */ + threatMatchesCount: number; + /** + * Threat enrichments (from the real time query) + */ + threatEnrichments: CtiEnrichment[]; + /** + * Threat enrichments count + */ + threatEnrichmentsCount: number; +} + +/** + * Hook to retrieve threat intelligence data for the expandable flyout right and left sections. + */ +export const useFetchThreatIntelligence = ({ + dataFormattedForFieldBrowser, +}: UseThreatIntelligenceParams): UseThreatIntelligenceValue => { + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + + // retrieve the threat enrichment fields with value for the current document + // (see https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/cti/constants.ts#L35) + const eventFields = useMemo( + () => getEnrichmentFields(dataFormattedForFieldBrowser || []), + [dataFormattedForFieldBrowser] + ); + + // retrieve existing enrichment fields and their value + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(dataFormattedForFieldBrowser || []).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [dataFormattedForFieldBrowser, isAlert] + ); + + // api call to retrieve all documents that match the eventFields + const { result: response, loading } = useInvestigationTimeEnrichment(eventFields); + + // combine existing enrichment and enrichment from the api response + // also removes the investigation-time enrichments if the exact indicator already exists + const allEnrichments = useMemo(() => { + if (loading || !response?.enrichments) { + return existingEnrichments; + } + return filterDuplicateEnrichments([...existingEnrichments, ...response.enrichments]); + }, [loading, response, existingEnrichments]); + + // separate threat matches (from indicator-match rule) from threat enrichments (realtime query) + const { + [ENRICHMENT_TYPES.IndicatorMatchRule]: threatMatches, + [ENRICHMENT_TYPES.InvestigationTime]: threatEnrichments, + } = groupBy(allEnrichments, 'matched.type'); + + return { + loading, + error: !dataFormattedForFieldBrowser, + threatMatches, + threatMatchesCount: (threatMatches || []).length, + threatEnrichments, + threatEnrichmentsCount: (threatEnrichments || []).length, + }; +};