From 5e4408473777227115bc9c110426258ef02a537c Mon Sep 17 00:00:00 2001 From: lgestc Date: Mon, 24 Apr 2023 18:13:37 +0200 Subject: [PATCH] [Security Solution] Flyout Insights - threats #6422 --- ...s_left_panel_threat_intelligence_tab.cy.ts | 28 ++++++ .../cti_details/threat_details_view.tsx | 14 ++- .../use_threat_intelligence_details.test.ts | 81 +++++++++++++++++ .../hooks/use_threat_intelligence_details.ts | 80 +++++++++++++++++ .../public/flyout/left/components/test_ids.ts | 24 +++-- .../threat_intelligence_details.test.tsx | 89 +++++++++++++++++++ .../threat_intelligence_details.tsx | 47 ++++++++-- .../public/flyout/left/context.tsx | 7 +- 8 files changed, 352 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/left/components/hooks/use_threat_intelligence_details.test.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/left/components/hooks/use_threat_intelligence_details.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.test.tsx diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts new file mode 100644 index 0000000000000..4139635f9dc2b --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts @@ -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'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 2602220ee1850..dd0af831e2b63 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -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 = , + showInvestigationTimeEnrichments, + loading, + children, +}) => { const { [ENRICHMENT_TYPES.IndicatorMatchRule]: indicatorMatches, [ENRICHMENT_TYPES.InvestigationTime]: threatIntelEnrichments, @@ -86,7 +96,7 @@ const ThreatDetailsViewComponent: React.FC<{ return ( <> - + {before} { + 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.isLoading).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'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/hooks/use_threat_intelligence_details.ts b/x-pack/plugins/security_solution/public/flyout/left/components/hooks/use_threat_intelligence_details.ts new file mode 100644 index 0000000000000..c98d1dab096d3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/left/components/hooks/use_threat_intelligence_details.ts @@ -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 isLoading = isEnrichmentsLoading || isEventDataLoading; + + return { + enrichments: allEnrichments, + eventFields, + isEnrichmentsLoading, + isEventDataLoading, + isLoading, + range, + setRange, + }; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts index 40cf67fddb180..f8301b317d03d 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts @@ -5,14 +5,20 @@ * 2.0. */ -export const ANALYZER_GRAPH_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnalyzerGraph'; -export const ANALYZE_GRAPH_ERROR_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutAnalyzerGraphError'; -export const SESSION_VIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionView'; -export const SESSION_VIEW_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionViewError'; -export const ENTITIES_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesDetails'; +const PREFIX = 'securitySolution' as const; + +export const ANALYZER_GRAPH_TEST_ID = `${PREFIX}DocumentDetailsFlyoutAnalyzerGraph` as const; +export const ANALYZE_GRAPH_ERROR_TEST_ID = `${PREFIX}DocumentDetailsFlyoutAnalyzerGraphError`; +export const SESSION_VIEW_TEST_ID = `${PREFIX}DocumentDetailsFlyoutSessionView` as const; +export const SESSION_VIEW_ERROR_TEST_ID = `${PREFIX}DocumentDetailsFlyoutSessionViewError` as const; +export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}DocumentDetailsFlyoutEntitiesDetails` as const; export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutThreatIntelligenceDetails'; -export const PREVALENCE_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutPrevalenceDetails'; + `${PREFIX}DocumentDetailsFlyoutThreatIntelligenceDetails` as const; +export const PREVALENCE_DETAILS_TEST_ID = + `${PREFIX}DocumentDetailsFlyoutPrevalenceDetails` as const; export const CORRELATIONS_DETAILS_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutCorrelationsDetails'; + `${PREFIX}DocumentDetailsFlyoutCorrelationsDetails` as const; + +export const THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID = `threat-match-detected` as const; +export const THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID = + `${PREFIX}ThreatIntelligenceDetailsLoadingSpinner` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.test.tsx new file mode 100644 index 0000000000000..4ba0a4bec98a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.test.tsx @@ -0,0 +1,89 @@ +/* + * 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, + THREAT_INTELLIGENCE_DETAILS_SPINNER_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(
), + }, + }, + }), + }; +}); + +jest.mock('./hooks/use_threat_intelligence_details'); + +const defaultContextValue = { + getFieldsData: () => 'id', +} as unknown as LeftPanelContext; + +// Renders System Under Test +const renderSUT = (contextValue: LeftPanelContext) => + render( + + + + + + ); + +describe('', () => { + it('should render the view', () => { + jest.mocked(useThreatIntelligenceDetails).mockReturnValue({ + isLoading: true, + enrichments: [], + isEventDataLoading: false, + isEnrichmentsLoading: true, + range: { from: '', to: '' }, + setRange: () => {}, + eventFields: {}, + }); + + const wrapper = renderSUT(defaultContextValue); + + expect( + wrapper.getByTestId(THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID) + ).toBeInTheDocument(); + + expect(useThreatIntelligenceDetails).toHaveBeenCalled(); + }); + + it('should render loading spinner when event details are pending', () => { + jest.mocked(useThreatIntelligenceDetails).mockReturnValue({ + isLoading: true, + enrichments: [], + isEventDataLoading: true, + isEnrichmentsLoading: true, + range: { from: '', to: '' }, + setRange: () => {}, + eventFields: {}, + }); + + const wrapper = renderSUT(defaultContextValue); + + expect(wrapper.getByTestId(THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID)).toBeInTheDocument(); + + expect(useThreatIntelligenceDetails).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.tsx index 01ce8894defec..9ef895ad34b3e 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/threat_intelligence_details.tsx @@ -6,17 +6,54 @@ */ import React from 'react'; -import { EuiText } from '@elastic/eui'; -import { THREAT_INTELLIGENCE_DETAILS_TEST_ID } from './test_ids'; - -export const THREAT_INTELLIGENCE_TAB_ID = 'threat-intelligence-details'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, 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'; +import { THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID } from './test_ids'; /** * Threat intelligence displayed in the document details expandable flyout left section under the Insights tab */ export const ThreatIntelligenceDetails: React.FC = () => { + const { + enrichments, + eventFields, + isEnrichmentsLoading, + isEventDataLoading, + isLoading, + range, + setRange, + } = useThreatIntelligenceDetails(); + + if (isEventDataLoading) { + return ( + + + + + + ); + } + return ( - {'Threat Intelligence'} + <> + + <> + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/left/context.tsx b/x-pack/plugins/security_solution/public/flyout/left/context.tsx index bf6adb5cd6a07..084dbc61e0421 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/context.tsx @@ -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(undefined); @@ -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) {