diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts index 55b4f9c6aca4d..43f66da6109df 100644 --- a/x-pack/plugins/siem/common/types/timeline/index.ts +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -144,6 +144,11 @@ export const TimelineTypeLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineType.default), ]); +const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt); + +export type TimelineTypeLiteral = runtimeTypes.TypeOf; +export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf; + export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts index a779d579bf4d1..a7c0b08fc8a21 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -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: {}, diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts index 16ba2de872bd1..681d39feb09f8 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts @@ -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: '', diff --git a/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts index e68db445a5cbb..d70a419b99a3b 100644 --- a/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -129,6 +129,9 @@ export const oneTimelineQuery = gql` version } title + timelineType + templateTimelineId + templateTimelineVersion savedQueryId sort { columnId diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 8c39d5e58b99e..86890988c06b6 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -5112,6 +5112,12 @@ export namespace GetOneTimeline { title: Maybe; + timelineType: Maybe; + + templateTimelineId: Maybe; + + templateTimelineVersion: Maybe; + savedQueryId: Maybe; sort: Maybe; diff --git a/x-pack/plugins/siem/public/mock/global_state.ts b/x-pack/plugins/siem/public/mock/global_state.ts index 6678c3043a3da..d0223b7834db0 100644 --- a/x-pack/plugins/siem/public/mock/global_state.ts +++ b/x-pack/plugins/siem/public/mock/global_state.ts @@ -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, diff --git a/x-pack/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts index edd1c73771829..1af0f533a7ca9 100644 --- a/x-pack/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -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, }, diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx index 8aaed08a0a0a1..ab75fcb6d6d1f 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx @@ -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, }, diff --git a/x-pack/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/store/timeline/defaults.ts index 7f04bb4c4dad0..9203720e2e28c 100644 --- a/x-pack/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/plugins/siem/public/store/timeline/defaults.ts @@ -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 { describe('#convertTimelineAsInput ', () => { @@ -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', }); }); diff --git a/x-pack/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts index 6812d8d8aa672..a7b8c48b45068 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.ts @@ -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 = (): 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, diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts index 19de49918d100..adab029c11150 100644 --- a/x-pack/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/plugins/siem/public/store/timeline/helpers.ts @@ -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 { diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts index 15bd2980e4aeb..7885064380eff 100644 --- a/x-pack/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -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' diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts index 58fc1c7e1e3df..42c6d6ecb0e51 100644 --- a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts +++ b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts @@ -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, diff --git a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index bde24a338ec84..00fb77bfb1647 100644 --- a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -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 => { diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index eeded1cc2532d..6de10bffb1325 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -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; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index 304ca309775ff..2827c7a1c0ac6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -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=', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 56c152d02ae98..11f93a9c48bf6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -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, }; }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index bff89bdf9b5b2..99621f1391acb 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -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`, }) ); } diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts index 9c47488d47159..2a3feb7afd59c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts @@ -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 diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index 9e120cdc023dc..a49627d40c8f5 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -96,4 +96,6 @@ export const timelineSavedObjectOmittedFields = [ 'createdBy', 'updated', 'updatedBy', + 'templateTimelineId', + 'templateTimelineVersion', ]; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts index 6a25d8def9116..a4efa676daddc 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts @@ -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 {