[SIEM] Fix auto save for template timeline (#65001)

* update save timeline

* fix types

* allow template timeline to be updated via import

* fix unit tests

* fix for review

* handle update timeline

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Angela Chuang 2020-05-05 09:42:17 +01:00 committed by GitHub
parent eae92e34c3
commit 537065a977
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 212 additions and 35 deletions

View file

@ -144,6 +144,11 @@ export const TimelineTypeLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TimelineType.default),
]);
const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt);
export type TimelineTypeLiteral = runtimeTypes.TypeOf<typeof TimelineTypeLiteralRt>;
export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf<typeof TimelineTypeLiteralWithNullRt>;
export const SavedTimelineRuntimeType = runtimeTypes.partial({
columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)),
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),

View file

@ -36,6 +36,7 @@ import { KueryFilterQueryKind } from '../../store/model';
import { Note } from '../../lib/note';
import moment from 'moment';
import sinon from 'sinon';
import { TimelineType } from '../../../common/types/timeline';
jest.mock('../../store/inputs/actions');
jest.mock('../../store/timeline/actions');
@ -299,6 +300,9 @@ describe('helpers', () => {
sortDirection: 'desc',
},
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
version: '1',
width: 1100,
});
@ -393,6 +397,9 @@ describe('helpers', () => {
sortDirection: 'desc',
},
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
version: '1',
width: 1100,
});
@ -467,6 +474,9 @@ describe('helpers', () => {
},
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
@ -632,6 +642,9 @@ describe('helpers', () => {
},
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},

View file

@ -8,8 +8,8 @@ import ApolloClient from 'apollo-client';
import { getOr, set, isEmpty } from 'lodash/fp';
import { Action } from 'typescript-fsa';
import uuid from 'uuid';
import { Dispatch } from 'redux';
import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query';
import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types';
import {
@ -169,6 +169,8 @@ export const defaultTimelineToTimelineModel = (
savedObjectId: duplicate ? null : timeline.savedObjectId,
version: duplicate ? null : timeline.version,
title: duplicate ? '' : timeline.title || '',
templateTimelineId: duplicate ? null : timeline.templateTimelineId,
templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion,
}).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), {
...timelineDefaults,
id: '',

View file

@ -129,6 +129,9 @@ export const oneTimelineQuery = gql`
version
}
title
timelineType
templateTimelineId
templateTimelineVersion
savedQueryId
sort {
columnId

View file

@ -5112,6 +5112,12 @@ export namespace GetOneTimeline {
title: Maybe<string>;
timelineType: Maybe<TimelineType>;
templateTimelineId: Maybe<string>;
templateTimelineVersion: Maybe<number>;
savedQueryId: Maybe<string>;
sort: Maybe<Sort>;

View file

@ -23,6 +23,7 @@ import {
DEFAULT_INTERVAL_TYPE,
DEFAULT_INTERVAL_VALUE,
} from '../../common/constants';
import { TimelineType } from '../../common/types/timeline';
export const mockGlobalState: State = {
app: {
@ -201,6 +202,9 @@ export const mockGlobalState: State = {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
dateRange: {
start: 0,

View file

@ -3,6 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter';
import { TimelineType } from '../../common/types/timeline';
import { OpenTimelineResult } from '../components/open_timeline/types';
import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../graphql/types';
@ -10,7 +13,6 @@ import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query';
import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types';
import { TimelineModel } from '../store/timeline/model';
import { timelineDefaults } from '../store/timeline/defaults';
import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter';
export interface MockedProvidedQuery {
request: {
query: GetAllTimeline.Query;
@ -168,7 +170,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 1',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -297,7 +299,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 2',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -426,7 +428,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 2',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -555,7 +557,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 3',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -684,7 +686,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 4',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -813,7 +815,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 5',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -942,7 +944,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 6',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1071,7 +1073,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1200,7 +1202,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1329,7 +1331,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1458,7 +1460,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1587,7 +1589,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -1716,7 +1718,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
'ZF0W12oB9v5HJNSHwY6L',
],
title: 'test 7',
timelineType: null,
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
created: 1558386787614,
@ -2141,6 +2143,9 @@ export const mockTimelineModel: TimelineModel = {
sortDirection: Direction.desc,
},
title: 'Test rule',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
version: '1',
width: 1100,
};
@ -2164,6 +2169,9 @@ export const mockTimelineResult: TimelineResult = {
],
kqlMode: 'filter',
title: 'Test rule',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
version: '1',
@ -2235,6 +2243,9 @@ export const defaultTimelineProps: CreateTimelineProps = {
showRowRenderers: true,
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
version: null,
width: 1100,
},

View file

@ -15,6 +15,7 @@ import {
} from '../../../../mock/';
import { CreateTimeline, UpdateTimelineLoading } from './types';
import { Ecs } from '../../../../graphql/types';
import { TimelineType } from '../../../../../common/types/timeline';
jest.mock('apollo-client');
@ -215,6 +216,9 @@ describe('signals actions', () => {
sortDirection: 'desc',
},
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
version: null,
width: 1100,
},

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TimelineType } from '../../../common/types/timeline';
import { Direction } from '../../graphql/types';
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants';
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
@ -33,6 +35,9 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
},
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},

View file

@ -4,12 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TimelineModel } from './model';
import { Direction } from '../../graphql/types';
import { convertTimelineAsInput } from './epic';
import { Filter, esFilters } from '../../../../../../src/plugins/data/public';
import { TimelineType } from '../../../common/types/timeline';
import { Direction } from '../../graphql/types';
import { TimelineModel } from './model';
import { convertTimelineAsInput } from './epic';
describe('Epic Timeline', () => {
describe('#convertTimelineAsInput ', () => {
test('should return a TimelineInput instead of TimelineModel ', () => {
@ -135,6 +138,9 @@ describe('Epic Timeline', () => {
},
loadingEventIds: [],
title: 'saved',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
@ -283,6 +289,9 @@ describe('Epic Timeline', () => {
columnId: '@timestamp',
sortDirection: 'desc',
},
templateTimelineId: null,
templateTimelineVersion: null,
timelineType: TimelineType.default,
title: 'saved',
});
});

View file

@ -29,6 +29,7 @@ import {
} from 'rxjs/operators';
import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public';
import { TimelineType } from '../../../common/types/timeline';
import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types';
import { AppApolloClient } from '../../lib/lib';
import { addError } from '../app/actions';
@ -236,6 +237,9 @@ export const createTimelineEpic = <State>(): Epic<
...savedTimeline,
savedObjectId: response.timeline.savedObjectId,
version: response.timeline.version,
timelineType: response.timeline.timelineType ?? TimelineType.default,
templateTimelineId: response.timeline.templateTimelineId ?? null,
templateTimelineVersion: response.timeline.templateTimelineVersion ?? null,
isSaving: false,
},
}),
@ -283,6 +287,9 @@ const timelineInput: TimelineInput = {
kqlMode: null,
kqlQuery: null,
title: null,
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
dateRange: null,
savedQueryId: null,
sort: null,

View file

@ -7,6 +7,7 @@
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
import { Filter } from '../../../../../../src/plugins/data/public';
import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers';
import { Sort } from '../../components/timeline/body/sort';
import {

View file

@ -5,6 +5,9 @@
*/
import { Filter } from '../../../../../../src/plugins/data/public';
import { TimelineTypeLiteralWithNull } from '../../../common/types/timeline';
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
import { Sort } from '../../components/timeline/body/sort';
import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types';
@ -77,6 +80,12 @@ export interface TimelineModel {
};
/** Title */
title: string;
/** timelineTypes: default | template */
timelineType: TimelineTypeLiteralWithNull;
/** an unique id for template timeline */
templateTimelineId: string | null;
/** null for default timeline, number for template timeline */
templateTimelineVersion: number | null;
/** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */
noteIds: string[];
/** Events pinned to this timeline */
@ -125,6 +134,9 @@ export type SubsetTimelineModel = Readonly<
| 'kqlMode'
| 'kqlQuery'
| 'title'
| 'timelineType'
| 'templateTimelineId'
| 'templateTimelineVersion'
| 'loadingEventIds'
| 'noteIds'
| 'pinnedEventIds'

View file

@ -6,6 +6,8 @@
import { cloneDeep, set } from 'lodash/fp';
import { TimelineType } from '../../../common/types/timeline';
import {
IS_OPERATOR,
DataProvider,
@ -80,6 +82,9 @@ const timelineByIdMock: TimelineById = {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
@ -1110,6 +1115,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,
@ -1202,6 +1210,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,
@ -1400,6 +1411,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,
@ -1492,6 +1506,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
noteIds: [],
dateRange: {
start: 0,
@ -1679,6 +1696,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,
@ -1755,6 +1775,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,
@ -1855,6 +1878,9 @@ describe('Timeline', () => {
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
noteIds: [],
dateRange: {
start: 0,

View file

@ -11,16 +11,21 @@ import { identity } from 'fp-ts/lib/function';
import {
TimelineSavedObjectRuntimeType,
TimelineSavedObject,
TimelineType,
} from '../../../common/types/timeline';
export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => {
const timeline = pipe(
TimelineSavedObjectRuntimeType.decode(savedObject),
map(savedTimeline => {
const attributes = {
...savedTimeline.attributes,
timelineType: savedTimeline.attributes.timelineType ?? TimelineType.default,
};
return {
savedObjectId: savedTimeline.id,
version: savedTimeline.version,
...savedTimeline.attributes,
...attributes,
};
}),
fold(errors => {

View file

@ -16,6 +16,7 @@ export const pickSavedTimeline = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any => {
const dateNow = new Date().valueOf();
if (timelineId == null) {
savedTimeline.created = dateNow;
savedTimeline.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER;
@ -27,13 +28,15 @@ export const pickSavedTimeline = (
}
if (savedTimeline.timelineType === TimelineType.template) {
savedTimeline.timelineType = TimelineType.template;
if (savedTimeline.templateTimelineId == null) {
// create template timeline
savedTimeline.templateTimelineId = uuid.v4();
}
if (savedTimeline.templateTimelineVersion == null) {
savedTimeline.templateTimelineVersion = 1;
} else {
// update template timeline
if (savedTimeline.templateTimelineVersion != null) {
savedTimeline.templateTimelineVersion = savedTimeline.templateTimelineVersion + 1;
}
}
} else {
savedTimeline.timelineType = TimelineType.default;

View file

@ -109,7 +109,7 @@ export const updateTemplateTimelineWithTimelineId = {
timeline: {
...inputTemplateTimeline,
templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
templateTimelineVersion: 2,
templateTimelineVersion: 1,
},
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
version: 'WzEyMjUsMV0=',

View file

@ -29,6 +29,7 @@ describe('import timelines', () => {
let securitySetup: SecurityPluginSetup;
let { context } = requestContextMock.createTools();
let mockGetTimeline: jest.Mock;
let mockGetTemplateTimeline: jest.Mock;
let mockPersistTimeline: jest.Mock;
let mockPersistPinnedEventOnTimeline: jest.Mock;
let mockPersistNote: jest.Mock;
@ -51,6 +52,7 @@ describe('import timelines', () => {
} as unknown) as SecurityPluginSetup;
mockGetTimeline = jest.fn();
mockGetTemplateTimeline = jest.fn();
mockPersistTimeline = jest.fn();
mockPersistPinnedEventOnTimeline = jest.fn();
mockPersistNote = jest.fn();
@ -83,6 +85,7 @@ describe('import timelines', () => {
jest.doMock('../saved_object', () => {
return {
getTimeline: mockGetTimeline.mockReturnValue(null),
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
persistTimeline: mockPersistTimeline.mockReturnValue({
timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion },
}),
@ -212,11 +215,12 @@ describe('import timelines', () => {
});
});
describe('Import a timeline already exist but overwrite is not allowed', () => {
describe('Import a timeline already exist', () => {
beforeEach(() => {
jest.doMock('../saved_object', () => {
return {
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
persistTimeline: mockPersistTimeline,
};
});

View file

@ -38,7 +38,9 @@ import {
PromiseFromStreams,
timelineSavedObjectOmittedFields,
} from './utils/import_timelines';
import { createTimelines, getTimeline } from './utils/create_timelines';
import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines';
import { TimelineType } from '../../../../common/types/timeline';
import { checkIsFailureCases } from './utils/update_timelines';
const CHUNK_PARSED_OBJECT_SIZE = 10;
@ -121,6 +123,9 @@ export const importTimelinesRoute = (
pinnedEventIds,
globalNotes,
eventNotes,
templateTimelineId,
templateTimelineVersion,
timelineType,
} = parsedTimeline;
const parsedTimelineObject = omit(
timelineSavedObjectOmittedFields,
@ -128,9 +133,23 @@ export const importTimelinesRoute = (
);
let newTimeline = null;
try {
const timeline = await getTimeline(frameworkRequest, savedObjectId);
if (timeline == null) {
const templateTimeline =
templateTimelineId != null
? await getTemplateTimeline(frameworkRequest, templateTimelineId)
: null;
const timeline =
templateTimeline?.savedObjectId != null || savedObjectId != null
? await getTimeline(
frameworkRequest,
templateTimeline?.savedObjectId ?? savedObjectId
)
: null;
const isHandlingTemplateTimeline = timelineType === TimelineType.template;
if (
(timeline == null && !isHandlingTemplateTimeline) ||
(templateTimeline == null && isHandlingTemplateTimeline)
) {
// create timeline / template timeline
newTimeline = await createTimelines(
frameworkRequest,
parsedTimelineObject,
@ -141,6 +160,37 @@ export const importTimelinesRoute = (
[] // existing note ids
);
resolve({
timeline_id: newTimeline.timeline.savedObjectId,
status_code: 200,
});
} else if (
timeline != null &&
templateTimeline != null &&
isHandlingTemplateTimeline
) {
// update template timeline
const errorObj = checkIsFailureCases(
isHandlingTemplateTimeline,
timeline.version,
templateTimeline.templateTimelineVersion ?? null,
timeline,
templateTimeline
);
if (errorObj != null) {
return siemResponse.error(errorObj);
}
newTimeline = await createTimelines(
frameworkRequest,
{ ...parsedTimelineObject, templateTimelineId, templateTimelineVersion },
timeline.savedObjectId, // timelineSavedObjectId
timeline.version, // timelineVersion
pinnedEventIds,
globalNotes,
[] // existing note ids
);
resolve({
timeline_id: newTimeline.timeline.savedObjectId,
status_code: 200,
@ -150,7 +200,7 @@ export const importTimelinesRoute = (
createBulkErrorObject({
id: savedObjectId,
statusCode: 409,
message: `timeline_id: "${savedObjectId}" already exists`,
message: `timeline_id: "${timeline?.savedObjectId}" already exists`,
})
);
}

View file

@ -215,6 +215,12 @@ describe('update timelines', () => {
);
});
test('should Update existing template timeline with timelineId', async () => {
expect(mockPersistTimeline.mock.calls[0][1]).toEqual(
updateTemplateTimelineWithTimelineId.timelineId
);
});
test('should Update existing template timeline with timeline version', async () => {
expect(mockPersistTimeline.mock.calls[0][2]).toEqual(
updateTemplateTimelineWithTimelineId.version

View file

@ -96,4 +96,6 @@ export const timelineSavedObjectOmittedFields = [
'createdBy',
'updated',
'updatedBy',
'templateTimelineId',
'templateTimelineVersion',
];

View file

@ -14,8 +14,7 @@ export const NO_MATCH_VERSION_ERROR_MESSAGE =
'TimelineVersion conflict: The given version doesn not match with existing timeline';
export const NO_MATCH_ID_ERROR_MESSAGE =
"Timeline id doesn't match with existing template timeline";
export const OLDER_VERSION_ERROR_MESSAGE =
'Template timelineVersion conflict: The given version is older then existing version';
export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
export const checkIsFailureCases = (
isHandlingTemplateTimeline: boolean,
@ -68,11 +67,11 @@ export const checkIsFailureCases = (
templateTimelineVersion != null &&
existTemplateTimeline != null &&
existTemplateTimeline.templateTimelineVersion != null &&
existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion
existTemplateTimeline.templateTimelineVersion !== templateTimelineVersion
) {
// Throw error you can not update a template timeline version with an old version
return {
body: OLDER_VERSION_ERROR_MESSAGE,
body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
statusCode: 409,
};
} else {