Skip to content

Commit

Permalink
[8.0] [Security Solution] Remove a data fetching hook from the add to…
Browse files Browse the repository at this point in the history
… timeline action component (#124331) (#125810)

* [Security Solution] Remove a data fetching hook from the add to timeline action component (#124331)

* Fetch alert ecs data in actions.tsx and not a hook in every table row

* Add error handling and tests for theshold timelines

* Fix bad merge

* Remove unused imports

* Actually remove unused file

* Remove usage of alertIds and dead code from cases

* Add basic sanity tests that ensure no extra network calls are being made

* Remove unused operator

* Remove unused imports

* Remove unused mock

(cherry picked from commit e312c36)

# Conflicts:
#	x-pack/plugins/cases/public/components/case_view/case_view_page.tsx
#	x-pack/plugins/cases/public/components/user_actions/types.ts
#	x-pack/plugins/security_solution/public/cases/pages/index.tsx
#	x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx

* Fix types

* Fix failing tests
  • Loading branch information
kqualters-elastic committed Feb 16, 2022
1 parent 090f7aa commit 0dbedb5
Show file tree
Hide file tree
Showing 23 changed files with 633 additions and 585 deletions.
2 changes: 0 additions & 2 deletions x-pack/plugins/cases/public/components/__mock__/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ export const timelineIntegrationMock = {
useInsertTimeline: jest.fn(),
},
ui: {
renderInvestigateInTimelineActionComponent: () =>
mockTimelineComponent('investigate-in-timeline'),
renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'),
},
};
Expand Down
3 changes: 0 additions & 3 deletions x-pack/plugins/cases/public/components/case_view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,6 @@ export const CaseComponent = React.memo<CaseComponentProps>(
isLoadingUserActions={isLoadingUserActions}
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
renderInvestigateInTimelineActionComponent={
timelineUi?.renderInvestigateInTimelineActionComponent
}
statusActionButton={
caseData.type !== CaseType.collection && userCanCrud ? (
<StatusActionButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export interface CasesTimelineIntegration {
) => UseInsertTimelineReturn;
};
ui?: {
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
renderTimelineDetailsPanel?: () => JSX.Element;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ import { timelineActions } from '../../../timelines/store/timeline';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
import { useFetchAlertData } from './helpers';
import { SEND_ALERT_TO_TIMELINE } from './translations';
import { useInsertTimeline } from '../use_insert_timeline';
import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline';
import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context';
Expand Down Expand Up @@ -68,17 +66,6 @@ const TimelineDetailsPanel = () => {
);
};

const InvestigateInTimelineActionComponent = (alertIds: string[]) => {
return (
<InvestigateInTimelineAction
ariaLabel={SEND_ALERT_TO_TIMELINE}
alertIds={alertIds}
key="investigate-in-timeline"
ecsRowData={null}
/>
);
};

export const CaseView = React.memo(
({ caseId, subCaseId, userCanCrud, onCaseDataSuccess }: Props) => {
const {
Expand Down Expand Up @@ -227,7 +214,6 @@ export const CaseView = React.memo(
useInsertTimeline,
},
ui: {
renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent,
renderTimelineDetailsPanel: TimelineDetailsPanel,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import { CoreStart } from '../../../../../../../src/core/public';
import { StartPlugins } from '../../../types';

type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings'> & Pick<StartPlugins, 'data'>;
type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings' | 'notifications'> &
Pick<StartPlugins, 'data'>;

export class KibanaServices {
private static kibanaVersion?: string;
Expand All @@ -19,8 +20,9 @@ export class KibanaServices {
data,
kibanaVersion,
uiSettings,
notifications,
}: GlobalServices & { kibanaVersion: string }) {
this.services = { data, http, uiSettings };
this.services = { data, http, uiSettings, notifications };
this.kibanaVersion = kibanaVersion;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,28 @@ import type { ISearchStart } from '../../../../../../../src/plugins/data/public'
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { getTimelineTemplate } from '../../../timelines/containers/api';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { KibanaServices } from '../../../common/lib/kibana';
import {
DEFAULT_FROM_MOMENT,
DEFAULT_TO_MOMENT,
} from '../../../common/utils/default_date_settings';

jest.mock('../../../timelines/containers/api', () => ({
getTimelineTemplate: jest.fn(),
}));

jest.mock('../../../common/lib/kibana');

describe('alert actions', () => {
const anchor = '2020-03-01T17:59:46.349Z';
const unix = moment(anchor).valueOf();
let createTimeline: CreateTimeline;
let updateTimelineIsLoading: UpdateTimelineLoading;
let searchStrategyClient: jest.Mocked<ISearchStart>;
let clock: sinon.SinonFakeTimers;
let mockKibanaServices: jest.Mock;
let fetchMock: jest.Mock;
let toastMock: jest.Mock;

beforeEach(() => {
// jest carries state between mocked implementations when using
Expand All @@ -52,6 +62,14 @@ describe('alert actions', () => {

createTimeline = jest.fn() as jest.Mocked<CreateTimeline>;
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
mockKibanaServices = KibanaServices.get as jest.Mock;

fetchMock = jest.fn();
toastMock = jest.fn();
mockKibanaServices.mockReturnValue({
http: { fetch: fetchMock },
notifications: { toasts: { addError: toastMock } },
});

searchStrategyClient = {
...dataPluginMock.createStartContract().search,
Expand Down Expand Up @@ -418,6 +436,59 @@ describe('alert actions', () => {
});

describe('determineToAndFrom', () => {
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
...mockAADEcsDataWithAlert,
kibana: {
alert: {
...mockAADEcsDataWithAlert.kibana?.alert,
rule: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
parameters: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
threshold: {
field: ['destination.ip'],
value: 1,
},
},
name: ['mock threshold rule'],
saved_id: [],
type: ['threshold'],
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
timeline_id: undefined,
timeline_title: undefined,
},
threshold_result: {
count: 99,
from: '2021-01-10T21:11:45.839Z',
cardinality: [
{
field: 'source.ip',
value: 1,
},
],
terms: [
{
field: 'destination.ip',
value: 1,
},
],
},
},
},
});
beforeEach(() => {
fetchMock.mockResolvedValue({
hits: {
hits: [
{
_id: ecsDataMockWithNoTemplateTimeline[0]._id,
_index: 'mock',
_source: ecsDataMockWithNoTemplateTimeline[0],
},
],
},
});
});
test('it uses ecs.Data.timestamp if one is provided', () => {
const ecsDataMock: Ecs = {
...mockEcsDataWithAlert,
Expand All @@ -438,47 +509,6 @@ describe('alert actions', () => {
});

test('it uses original_time and threshold_result.from for threshold alerts', async () => {
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
...mockAADEcsDataWithAlert,
kibana: {
alert: {
...mockAADEcsDataWithAlert.kibana?.alert,
rule: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
parameters: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
threshold: {
field: ['destination.ip'],
value: 1,
},
},
name: ['mock threshold rule'],
saved_id: [],
type: ['threshold'],
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
timeline_id: undefined,
timeline_title: undefined,
},
threshold_result: {
count: 99,
from: '2021-01-10T21:11:45.839Z',
cardinality: [
{
field: 'source.ip',
value: 1,
},
],
terms: [
{
field: 'destination.ip',
value: 1,
},
],
},
},
},
});

const expectedFrom = '2021-01-10T21:11:45.839Z';
const expectedTo = '2021-01-10T21:12:45.839Z';

Expand Down Expand Up @@ -525,4 +555,86 @@ describe('alert actions', () => {
});
});
});

describe('show toasts when data is malformed', () => {
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
...mockAADEcsDataWithAlert,
kibana: {
alert: {
...mockAADEcsDataWithAlert.kibana?.alert,
rule: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
parameters: {
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
threshold: {
field: ['destination.ip'],
value: 1,
},
},
name: ['mock threshold rule'],
saved_id: [],
type: ['threshold'],
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
timeline_id: undefined,
timeline_title: undefined,
},
threshold_result: {
count: 99,
from: '2021-01-10T21:11:45.839Z',
cardinality: [
{
field: 'source.ip',
value: 1,
},
],
terms: [
{
field: 'destination.ip',
value: 1,
},
],
},
},
},
});
beforeEach(() => {
fetchMock.mockResolvedValue({
hits: 'not correctly formed doc',
});
});
test('renders a toast and calls create timeline with basic defaults', async () => {
const expectedFrom = DEFAULT_FROM_MOMENT.toISOString();
const expectedTo = DEFAULT_TO_MOMENT.toISOString();
const timelineProps = {
...defaultTimelineProps,
timeline: {
...defaultTimelineProps.timeline,
dataProviders: [],
dateRange: {
start: expectedFrom,
end: expectedTo,
},
description: '',
kqlQuery: {
filterQuery: null,
},
resolveTimelineConfig: undefined,
},
from: expectedFrom,
to: expectedTo,
};

delete timelineProps.ruleNote;

await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithNoTemplateTimeline,
updateTimelineIsLoading,
searchStrategyClient,
});
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith(timelineProps);
expect(toastMock).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 0dbedb5

Please sign in to comment.