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 5e44084
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 18 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.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');
});
});
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 isLoading = isEnrichmentsLoading || isEventDataLoading;

return {
enrichments: allEnrichments,
eventFields,
isEnrichmentsLoading,
isEventDataLoading,
isLoading,
range,
setRange,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -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(<div />),
},
},
}),
};
});

jest.mock('./hooks/use_threat_intelligence_details');

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', () => {
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();
});
});
Loading

0 comments on commit 5e44084

Please sign in to comment.