Skip to content

Commit

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

* 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)
  • Loading branch information
kqualters-elastic authored Feb 16, 2022
1 parent 1c23e80 commit a42b3f4
Show file tree
Hide file tree
Showing 18 changed files with 579 additions and 253 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
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,6 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
isLoadingUserActions={isLoadingUserActions}
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
renderInvestigateInTimelineActionComponent={
timelineUi?.renderInvestigateInTimelineActionComponent
}
statusActionButton={
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 @@ -28,7 +28,6 @@ export interface UserActionTreeProps {
onRuleDetailsClick?: RuleDetailsNavigation['onClick'];
onShowAlertDetails: (alertId: string, index: string) => void;
onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void;
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
statusActionButton: JSX.Element | null;
updateCase: (newCase: Case) => void;
useFetchAlertData: UseFetchAlertData;
Expand Down
14 changes: 0 additions & 14 deletions x-pack/plugins/security_solution/public/cases/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { TimelineId } from '../../../common/types/timeline';

import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to';

import * as i18n from './translations';
import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana';
import { APP_ID, CASES_PATH, SecurityPageName } from '../../../common/constants';
import { timelineActions } from '../../timelines/store/timeline';
Expand All @@ -25,7 +24,6 @@ import { SpyRoute } from '../../common/utils/route/spy_routes';
import { useInsertTimeline } from '../components/use_insert_timeline';
import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline';
import { DetailsPanel } from '../../timelines/components/side_panel';
import { InvestigateInTimelineAction } from '../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
import { useFetchAlertData } from './use_fetch_alert_data';

const TimelineDetailsPanel = () => {
Expand All @@ -44,17 +42,6 @@ const TimelineDetailsPanel = () => {
);
};

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

const CaseContainerComponent: React.FC = () => {
const { cases: casesUi } = useKibana().services;
const { getAppUrl, navigateTo } = useNavigation();
Expand Down Expand Up @@ -163,7 +150,6 @@ const CaseContainerComponent: React.FC = () => {
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 a42b3f4

Please sign in to comment.