mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] add threat intelligence overview to expandable flyout (#155328)
This commit is contained in:
parent
8a3f5ebbea
commit
4eeec1865f
15 changed files with 1250 additions and 0 deletions
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<InsightsSectionProps> = ({ expanded = fal
|
|||
return (
|
||||
<ExpandableSection title={INSIGHTS_TITLE} expanded={expanded} data-test-subj={INSIGHTS_TEST_ID}>
|
||||
<EntitiesOverview />
|
||||
<ThreatIntelligenceOverview />
|
||||
</ExpandableSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 = <div>{'hello'}</div>;
|
||||
|
||||
export const Basic: Story<void> = () => {
|
||||
return <InsightsSubSection title={title}>{children}</InsightsSubSection>;
|
||||
};
|
||||
|
||||
export const Loading: Story<void> = () => {
|
||||
return (
|
||||
<InsightsSubSection loading={true} title={title}>
|
||||
{null}
|
||||
</InsightsSubSection>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoTitle: Story<void> = () => {
|
||||
return <InsightsSubSection title={''}>{children}</InsightsSubSection>;
|
||||
};
|
||||
|
||||
export const NoChildren: Story<void> = () => {
|
||||
return <InsightsSubSection title={title}>{null}</InsightsSubSection>;
|
||||
};
|
|
@ -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 = <div>{'hello'}</div>;
|
||||
|
||||
describe('<InsightsSubSection />', () => {
|
||||
it('should render children component', () => {
|
||||
const { getByTestId } = render(
|
||||
<InsightsSubSection title={title} data-test-subj={dataTestSubj}>
|
||||
{children}
|
||||
</InsightsSubSection>
|
||||
);
|
||||
|
||||
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(
|
||||
<InsightsSubSection loading={true} title={title} data-test-subj={dataTestSubj}>
|
||||
{children}
|
||||
</InsightsSubSection>
|
||||
);
|
||||
|
||||
const loadingDataTestSubj = `${dataTestSubj}Loading`;
|
||||
expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render null if error', () => {
|
||||
const { container } = render(
|
||||
<InsightsSubSection error={true} title={title}>
|
||||
{children}
|
||||
</InsightsSubSection>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render null if no title', () => {
|
||||
const { container } = render(<InsightsSubSection title={''}>{children}</InsightsSubSection>);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render null if no children', () => {
|
||||
const { container } = render(
|
||||
<InsightsSubSection error={true} title={title}>
|
||||
{null}
|
||||
</InsightsSubSection>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
|
@ -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<InsightsSectionProps> = ({
|
||||
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 (
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner data-test-subj={loadingDataTestSubj} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
// hide everything
|
||||
if (error || !title || !children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const titleDataTestSubj = `${dataTestSubj}Title`;
|
||||
const contentDataTestSubj = `${dataTestSubj}Content`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxs" data-test-subj={titleDataTestSubj}>
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup data-test-subj={contentDataTestSubj} direction="column" gutterSize="s">
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InsightsSubSection.displayName = 'InsightsSubSection';
|
|
@ -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<void> = () => {
|
||||
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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvalidColor: Story<void> = () => {
|
||||
const data: InsightsSummaryPanelData[] = [
|
||||
{
|
||||
icon: 'image',
|
||||
value: 1,
|
||||
text: 'this is a test for an invalid color (abc)',
|
||||
color: 'abc',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoColor: Story<void> = () => {
|
||||
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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LongText: Story<void> = () => {
|
||||
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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const LongNumber: Story<void> = () => {
|
||||
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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoData: Story<void> = () => {
|
||||
const data: InsightsSummaryPanelData[] = [];
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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('<SummaryPanel />', () => {
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<InsightsSummaryPanel data={data} data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
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(<InsightsSummaryPanel data={data} />);
|
||||
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<InsightsSummaryPanel data={data} data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<InsightsSummaryPanel data={data} data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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<InsightsSummaryPanelProps> = ({
|
||||
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 (
|
||||
<EuiPanel hasShadow={false} hasBorder={true} paddingSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{data.map((row, index) => (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
justifyContent={'spaceBetween'}
|
||||
alignItems={'center'}
|
||||
key={index}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={iconDataTestSubj + index}
|
||||
aria-label={'entity-icon'}
|
||||
color="text"
|
||||
display="empty"
|
||||
iconType={row.icon}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
data-test-subj={valueDataTestSubj + index}
|
||||
css={css`
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<FormattedCount count={row.value} /> {row.text}
|
||||
</EuiFlexItem>
|
||||
{row.color && (
|
||||
<EuiFlexItem grow={false} data-test-subj={colorDataTestSubj + index}>
|
||||
<EuiHealth color={row.color} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
InsightsSummaryPanel.displayName = 'InsightsSummaryPanel';
|
|
@ -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 =
|
||||
|
|
|
@ -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) => (
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={contextValue}>
|
||||
<ThreatIntelligenceOverview />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('<ThreatIntelligenceOverview />', () => {
|
||||
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(
|
||||
<TestProviders>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<ThreatIntelligenceOverview />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<InsightsSubSection
|
||||
loading={loading}
|
||||
error={error}
|
||||
title={THREAT_INTELLIGENCE_TITLE}
|
||||
data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}
|
||||
>
|
||||
<InsightsSummaryPanel data={data} data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} />
|
||||
<EuiButtonEmpty
|
||||
onClick={goToThreatIntelligenceTab}
|
||||
iconType="arrowStart"
|
||||
iconSide="left"
|
||||
size="s"
|
||||
data-test-subj={`${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`}
|
||||
>
|
||||
{VIEW_ALL(THREAT_INTELLIGENCE_TEXT)}
|
||||
</EuiButtonEmpty>
|
||||
</InsightsSubSection>
|
||||
);
|
||||
};
|
||||
|
||||
ThreatIntelligenceOverview.displayName = 'ThreatIntelligenceOverview';
|
|
@ -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 },
|
||||
|
|
|
@ -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<UseThreatIntelligenceParams, UseThreatIntelligenceValue>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue