Skip to content

Commit

Permalink
[Security Solution] Flyout Insights - threats #6422
Browse files Browse the repository at this point in the history
  • Loading branch information
lgestc committed Apr 27, 2023
1 parent 54457b0 commit 01b59a8
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { cleanKibana } from '../../../tasks/common';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
import { expandFirstAlertExpandableFlyout } from '../../../tasks/document_expandable_flyout';
import { login, visit } from '../../../tasks/login';
import { ALERTS_URL } from '../../../urls/navigation';

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

it('should serialize its state to url', () => {
cy.url().should('include', 'eventFlyout');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,18 @@ const ThreatDetailsViewComponent: React.FC<{
enrichments: CtiEnrichment[];
showInvestigationTimeEnrichments: boolean;
loading: boolean;
/**
* Slot to render something before the beforeHeader
*/
before?: React.ReactNode;
children?: React.ReactNode;
}> = ({ enrichments, showInvestigationTimeEnrichments, loading, children }) => {
}> = ({
enrichments,
before = <EuiSpacer size="m" />,
showInvestigationTimeEnrichments,
loading,
children,
}) => {
const {
[ENRICHMENT_TYPES.IndicatorMatchRule]: indicatorMatches,
[ENRICHMENT_TYPES.InvestigationTime]: threatIntelEnrichments,
Expand All @@ -86,7 +96,7 @@ const ThreatDetailsViewComponent: React.FC<{

return (
<>
<EuiSpacer size="m" />
{before}
<EnrichmentSection
dataTestSubj="threat-match-detected"
enrichments={indicatorMatches}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 { useThreatIntelligenceDetails } from './use_threat_intelligence_details';
import { renderHook } from '@testing-library/react-hooks';

import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
import { useLeftPanelContext } from '../../context';
import { useInvestigationTimeEnrichment } from '../../../../common/containers/cti/event_enrichment';
import { SecurityPageName } from '../../../../../common/constants';
import type { RouteSpyState } from '../../../../common/utils/route/types';

jest.mock('../../../../timelines/containers/details');
jest.mock('../../../../common/containers/sourcerer');
jest.mock('../../../../common/utils/route/use_route_spy');
jest.mock('../../context');
jest.mock('../../../../common/containers/cti/event_enrichment');

describe('useThreatIntelligenceDetails', () => {
beforeEach(() => {
jest.mocked(useInvestigationTimeEnrichment).mockReturnValue({
result: { enrichments: [] },
loading: false,
setRange: jest.fn(),
range: { from: '2023-04-27T00:00:00Z', to: '2023-04-27T23:59:59Z' },
});

jest
.mocked(useTimelineEventsDetails)
.mockReturnValue([false, [], undefined, null, async () => {}]);

jest.mocked(useSourcererDataView).mockReturnValue({
runtimeMappings: {},
browserFields: {},
dataViewId: '',
loading: false,
indicesExist: true,
patternList: [],
selectedPatterns: [],
indexPattern: { fields: [], title: '' },
});

jest
.mocked(useRouteSpy)
.mockReturnValue([
{ pageName: SecurityPageName.detections } as unknown as RouteSpyState,
() => {},
]);

jest.mocked(useLeftPanelContext).mockReturnValue({
indexName: 'test-index',
eventId: 'test-event-id',
getFieldsData: () => {},
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('returns the expected values', () => {
const { result } = renderHook(() => useThreatIntelligenceDetails());

expect(result.current.enrichments).toEqual([]);
expect(result.current.eventFields).toEqual({});
expect(result.current.isEnrichmentsLoading).toBe(false);
expect(result.current.isEventDataLoading).toBe(false);
expect(result.current.loading).toBe(false);
expect(result.current.range).toEqual({
from: '2023-04-27T00:00:00Z',
to: '2023-04-27T23:59:59Z',
});
expect(typeof result.current.setRange).toBe('function');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 {
filterDuplicateEnrichments,
getEnrichmentFields,
parseExistingEnrichments,
timelineDataToEnrichment,
} from '../../../../common/components/event_details/cti_details/helpers';
import { SecurityPageName } from '../../../../../common/constants';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';

import { useInvestigationTimeEnrichment } from '../../../../common/containers/cti/event_enrichment';
import { useTimelineEventsDetails } from '../../../../timelines/containers/details';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
import { useLeftPanelContext } from '../../context';

export const useThreatIntelligenceDetails = () => {
const isAlert = true;

const { indexName, eventId } = useLeftPanelContext();
const [{ pageName }] = useRouteSpy();
const sourcererScope =
pageName === SecurityPageName.detections
? SourcererScopeName.detections
: SourcererScopeName.default;
const sourcererDataView = useSourcererDataView(sourcererScope);

const [isEventDataLoading, eventData] = useTimelineEventsDetails({
indexName,
eventId,
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !eventId,
});

const data = useMemo(() => eventData || [], [eventData]);
const eventFields = useMemo(() => getEnrichmentFields(data || []), [data]);

const {
result: enrichmentsResponse,
loading: isEnrichmentsLoading,
setRange,
range,
} = useInvestigationTimeEnrichment(eventFields);

const existingEnrichments = useMemo(
() =>
isAlert
? parseExistingEnrichments(data).map((enrichmentData) =>
timelineDataToEnrichment(enrichmentData)
)
: [],
[data, isAlert]
);

const allEnrichments = useMemo(() => {
if (isEnrichmentsLoading || !enrichmentsResponse?.enrichments) {
return existingEnrichments;
}
return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]);
}, [isEnrichmentsLoading, enrichmentsResponse, existingEnrichments]);

const loading = isEnrichmentsLoading || isEventDataLoading;

return {
enrichments: allEnrichments,
eventFields,
isEnrichmentsLoading,
isEventDataLoading,
loading,
range,
setRange,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export const THREAT_INTELLIGENCE_DETAILS_TEST_ID =
export const PREVALENCE_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutPrevalenceDetails';
export const CORRELATIONS_DETAILS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutCorrelationsDetails';

export const THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID = 'threat-match-detected' as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 '@testing-library/jest-dom';
import type { LeftPanelContext } from '../context';
import { LeftFlyoutContext } from '../context';
import { TestProviders } from '../../../common/mock';
import { THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID } from './test_ids';
import { ThreatIntelligenceDetails } from './threat_intelligence_details';
import { useThreatIntelligenceDetails } from './hooks/use_threat_intelligence_details';

jest.mock('../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../common/lib/kibana');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
services: {
sessionView: {
getSessionView: jest.fn().mockReturnValue(<div />),
},
},
}),
};
});

jest.mock('./hooks/use_threat_intelligence_details', () => ({
useThreatIntelligenceDetails: jest.fn(() => ({ loading: true })),
}));

const defaultContextValue = {
getFieldsData: () => 'id',
} as unknown as LeftPanelContext;

// Renders System Under Test
const renderSUT = (contextValue: LeftPanelContext) =>
render(
<TestProviders>
<LeftFlyoutContext.Provider value={contextValue}>
<ThreatIntelligenceDetails />
</LeftFlyoutContext.Provider>
</TestProviders>
);

describe('<ThreatIntelligenceDetails />', () => {
it('should render the view', () => {
const wrapper = renderSUT(defaultContextValue);

expect(
wrapper.getByTestId(THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID)
).toBeInTheDocument();

expect(useThreatIntelligenceDetails).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,36 @@
*/

import React from 'react';
import { EuiText } from '@elastic/eui';
import { THREAT_INTELLIGENCE_DETAILS_TEST_ID } from './test_ids';
import { EuiSpacer } from '@elastic/eui';
import isEmpty from 'lodash/isEmpty';
import { EnrichmentRangePicker } from '../../../common/components/event_details/cti_details/enrichment_range_picker';
import { ThreatDetailsView } from '../../../common/components/event_details/cti_details/threat_details_view';
import { useThreatIntelligenceDetails } from './hooks/use_threat_intelligence_details';

export const THREAT_INTELLIGENCE_TAB_ID = 'threat-intelligence-details';
// TODO: investigate using esClient in cypress
// TODO: add unit tests for this component and the hook

/**
* Threat intelligence displayed in the document details expandable flyout left section under the Insights tab
*/
export const ThreatIntelligenceDetails: React.FC = () => {
const { enrichments, eventFields, isEnrichmentsLoading, loading, range, setRange } =
useThreatIntelligenceDetails();

return (
<EuiText data-test-subj={THREAT_INTELLIGENCE_DETAILS_TEST_ID}>{'Threat Intelligence'}</EuiText>
<>
<ThreatDetailsView
before={null}
loading={loading}
enrichments={enrichments}
showInvestigationTimeEnrichments={!isEmpty(eventFields)}
>
<>
<EnrichmentRangePicker setRange={setRange} loading={isEnrichmentsLoading} range={range} />
<EuiSpacer size="m" />
</>
</ThreatDetailsView>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface LeftPanelContext {
* Retrieves searchHit values for the provided field
*/
getFieldsData: (field: string) => unknown | unknown[];

data?: unknown;
}

export const LeftFlyoutContext = createContext<LeftPanelContext | undefined>(undefined);
Expand Down Expand Up @@ -60,8 +62,9 @@ export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProvider
const getFieldsData = useGetFieldsData(searchHit?.fields);

const contextValue = useMemo(
() => (id && indexName ? { eventId: id, indexName, getFieldsData } : undefined),
[id, indexName, getFieldsData]
() =>
id && indexName ? { eventId: id, indexName, getFieldsData, data: searchHit } : undefined,
[id, indexName, getFieldsData, searchHit]
);

if (loading) {
Expand Down

0 comments on commit 01b59a8

Please sign in to comment.