diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index a832c818d48b0..b22fc543ddc29 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -138,6 +138,7 @@ export const mockGetTimelineValue = { kqlMode: 'filter', kqlQuery: { filterQuery: [] }, title: 'My duplicate timeline', + timelineType: TimelineType.default, dateRange: { start: 1584523907294, end: 1584610307294 }, savedQueryId: null, sort: { columnId: '@timestamp', sortDirection: 'desc' }, @@ -152,7 +153,7 @@ export const mockGetTimelineValue = { export const mockGetTemplateTimelineValue = { ...mockGetTimelineValue, timelineType: TimelineType.template, - templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineId: '79deb4c0-6bc1-0000-0000-f5341fb7a189', templateTimelineVersion: 1, }; 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 2827c7a1c0ac6..f5db8c2f804d4 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 @@ -96,7 +96,6 @@ export const createTimelineWithTimelineId = { export const createTemplateTimelineWithTimelineId = { ...createTemplateTimelineWithoutTimelineId, timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', - templateTimelineId: 'existing template timeline id', }; export const updateTimelineWithTimelineId = { @@ -108,7 +107,7 @@ export const updateTimelineWithTimelineId = { export const updateTemplateTimelineWithTimelineId = { timeline: { ...inputTemplateTimeline, - templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineId: '79deb4c0-6bc1-0000-0000-f5341fb7a189', templateTimelineVersion: 1, }, timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts index 70ee1532395a5..f5345c3dce222 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts @@ -23,6 +23,7 @@ import { createTimelineWithTimelineId, createTemplateTimelineWithoutTimelineId, createTemplateTimelineWithTimelineId, + updateTemplateTimelineWithTimelineId, } from './__mocks__/request_responses'; import { CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, @@ -34,6 +35,7 @@ describe('create 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; @@ -55,6 +57,7 @@ describe('create timelines', () => { } as unknown) as SecurityPluginSetup; mockGetTimeline = jest.fn(); + mockGetTemplateTimeline = jest.fn(); mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); @@ -231,11 +234,14 @@ describe('create timelines', () => { }); }); - describe('Import a template timeline already exist', () => { + describe('Create a template timeline already exist', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [mockGetTemplateTimelineValue], + }), persistTimeline: mockPersistTimeline, }; }); @@ -259,7 +265,7 @@ describe('create timelines', () => { test('returns error message', async () => { const response = await server.inject( - getCreateTimelinesRequest(createTemplateTimelineWithTimelineId), + getCreateTimelinesRequest(updateTemplateTimelineWithTimelineId), context ); expect(response.body).toEqual({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts index c456ae31fb7da..fcc61a22a8932 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts @@ -15,14 +15,8 @@ import { buildRouteValidation } from '../../../utils/build_validation/route_vali import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; import { createTimelineSchema } from './schemas/create_timelines_schema'; -import { buildFrameworkRequest } from './utils/common'; -import { - createTimelines, - getTimeline, - getTemplateTimeline, - CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, - CREATE_TIMELINE_ERROR_MESSAGE, -} from './utils/create_timelines'; +import { buildFrameworkRequest, TimelinesStatus, TimelineStatusActions } from './utils/common'; +import { createTimelines } from './utils/create_timelines'; export const createTimelinesRoute = ( router: IRouter, @@ -46,37 +40,43 @@ export const createTimelinesRoute = ( const frameworkRequest = await buildFrameworkRequest(context, security, request); const { timelineId, timeline, version } = request.body; - const { templateTimelineId, timelineType } = timeline; - const isHandlingTemplateTimeline = timelineType === TimelineType.template; + const { templateTimelineId, templateTimelineVersion, timelineType } = timeline; - const existTimeline = - timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; - const existTemplateTimeline = - templateTimelineId != null - ? await getTemplateTimeline(frameworkRequest, templateTimelineId) - : null; + const timelineStatus = new TimelinesStatus({ + timelineType: timelineType ?? TimelineType.default, + timelineInput: { + id: timelineId ?? null, + type: TimelineType.default, + version: version ?? null, + }, + templateTimelineInput: { + id: templateTimelineId ?? null, + type: TimelineType.template, + version: templateTimelineVersion ?? null, + }, + frameworkRequest, + }); - if ( - (!isHandlingTemplateTimeline && existTimeline != null) || - (isHandlingTemplateTimeline && (existTemplateTimeline != null || existTimeline != null)) - ) { - return siemResponse.error({ - body: isHandlingTemplateTimeline - ? CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE - : CREATE_TIMELINE_ERROR_MESSAGE, - statusCode: 405, - }); - } + await timelineStatus.setAvailableActions(); // Create timeline - const newTimeline = await createTimelines(frameworkRequest, timeline, null, version); - return response.ok({ - body: { - data: { - persistTimeline: newTimeline, + if (timelineStatus.isCreatable) { + const newTimeline = await createTimelines(frameworkRequest, timeline, null, version); + return response.ok({ + body: { + data: { + persistTimeline: newTimeline, + }, }, - }, - }); + }); + } else { + return siemResponse.error( + timelineStatus.checkIsFailureCases(TimelineStatusActions.create) || { + statusCode: 405, + body: 'update timeline error', + } + ); + } } catch (err) { const error = transformError(err); 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 4a79dada07171..4fb5560b535b7 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 @@ -28,7 +28,7 @@ import { import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { buildFrameworkRequest } from './utils/common'; +import { buildFrameworkRequest, TimelinesStatus, TimelineStatusActions } from './utils/common'; import { getTupleDuplicateErrorsAndUniqueTimeline, isBulkError, @@ -38,9 +38,8 @@ import { PromiseFromStreams, timelineSavedObjectOmittedFields, } from './utils/import_timelines'; -import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { createTimelines } from './utils/create_timelines'; import { TimelineType } from '../../../../common/types/timeline'; -import { checkIsFailureCases } from './utils/update_timelines'; const CHUNK_PARSED_OBJECT_SIZE = 10; @@ -123,9 +122,9 @@ export const importTimelinesRoute = ( pinnedEventIds, globalNotes, eventNotes, - templateTimelineId, - templateTimelineVersion, - timelineType, + templateTimelineId = null, + templateTimelineVersion = null, + timelineType = TimelineType.default, version = null, } = parsedTimeline; const parsedTimelineObject = omit( @@ -135,20 +134,24 @@ export const importTimelinesRoute = ( let newTimeline = null; try { - const templateTimeline = - templateTimelineId != null - ? await getTemplateTimeline(frameworkRequest, templateTimelineId) - : null; + const timelineStatus = new TimelinesStatus({ + timelineType, + timelineInput: { + id: savedObjectId, + type: TimelineType.default, + version, + }, + templateTimelineInput: { + id: templateTimelineId, + type: TimelineType.template, + version: templateTimelineVersion, + }, + frameworkRequest, + }); - const timeline = - savedObjectId != null && - (await getTimeline(frameworkRequest, savedObjectId)); - const isHandlingTemplateTimeline = timelineType === TimelineType.template; + await timelineStatus.setAvailableActions(); - if ( - (timeline == null && !isHandlingTemplateTimeline) || - (timeline == null && templateTimeline == null && isHandlingTemplateTimeline) - ) { + if (timelineStatus.isCreatableViaImport) { // create timeline / template timeline newTimeline = await createTimelines( frameworkRequest, @@ -156,7 +159,7 @@ export const importTimelinesRoute = ( null, // timelineSavedObjectId null, // timelineVersion pinnedEventIds, - isHandlingTemplateTimeline + timelineStatus.isHandlingTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], [] // existing note ids @@ -166,32 +169,33 @@ export const importTimelinesRoute = ( timeline_id: newTimeline.timeline.savedObjectId, status_code: 200, }); - } else if ( - timeline && - timeline != null && - templateTimeline != null && - isHandlingTemplateTimeline - ) { - // update template timeline - const errorObj = checkIsFailureCases( - isHandlingTemplateTimeline, - version, - templateTimelineVersion ?? null, - timeline, - templateTimeline + } else if (!timelineStatus.isHandlingTemplateTimeline) { + timelineStatus.checkIsFailureCases(TimelineStatusActions.createViaImport); + const message = + timelineStatus?.errorMessage?.body != null + ? timelineStatus.errorMessage.body + : `${timelineType} timeline ${TimelineStatusActions.createViaImport} error`; + + resolve( + createBulkErrorObject({ + id: savedObjectId ?? 'unknown', + statusCode: 409, + message, + }) ); - if (errorObj != null) { - return siemResponse.error(errorObj); - } + } + + if (timelineStatus.isUpdatableViaImport) { + // update template timeline newTimeline = await createTimelines( frameworkRequest, - { ...parsedTimelineObject, templateTimelineId, templateTimelineVersion }, - timeline.savedObjectId, // timelineSavedObjectId - timeline.version, // timelineVersion + parsedTimelineObject, + timelineStatus.timelineInput.id, // timelineSavedObjectId + timelineStatus.timelineInput.version?.toString() ?? null, // timelineVersion pinnedEventIds, globalNotes, - [] // existing note ids + timelineStatus.timelineInput?.data?.noteIds ?? [] // existing note ids ); resolve({ @@ -199,11 +203,17 @@ export const importTimelinesRoute = ( status_code: 200, }); } else { + timelineStatus.checkIsFailureCases(TimelineStatusActions.updateViaImport); + const message = + timelineStatus?.errorMessage?.body != null + ? timelineStatus.errorMessage.body + : `${timelineType} timeline ${TimelineStatusActions.updateViaImport} error`; + resolve( createBulkErrorObject({ id: savedObjectId ?? 'unknown', statusCode: 409, - message: `timeline_id: "${savedObjectId}" already exists`, + message, }) ); } @@ -253,7 +263,6 @@ export const importTimelinesRoute = ( } catch (err) { const error = transformError(err); const siemResponse = buildSiemResponse(response); - return siemResponse.error({ body: error.message, statusCode: error.statusCode, 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 2a3feb7afd59c..dca2f74a73563 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 @@ -26,7 +26,7 @@ import { import { UPDATE_TIMELINE_ERROR_MESSAGE, UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, -} from './utils/update_timelines'; +} from './utils/failure_cases'; describe('update timelines', () => { let server: ReturnType; @@ -178,7 +178,7 @@ describe('update timelines', () => { timeline: [mockGetTemplateTimelineValue], }), persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: updateTimelineWithTimelineId.timeline, + timeline: updateTemplateTimelineWithTimelineId.timeline, }), }; }); @@ -211,7 +211,7 @@ describe('update timelines', () => { test('should Update existing template timeline with template timelineId', async () => { expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( - updateTemplateTimelineWithTimelineId.timelineId + updateTemplateTimelineWithTimelineId.timeline.templateTimelineId ); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts index a0f3d11a1533d..420a551ebb40b 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts @@ -17,9 +17,8 @@ import { transformError, buildSiemResponse } from '../../detection_engine/routes import { FrameworkRequest } from '../../framework'; import { updateTimelineSchema } from './schemas/update_timelines_schema'; -import { buildFrameworkRequest } from './utils/common'; -import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { checkIsFailureCases } from './utils/update_timelines'; +import { buildFrameworkRequest, TimelinesStatus, TimelineStatusActions } from './utils/common'; +import { createTimelines } from './utils/create_timelines'; export const updateTimelinesRoute = ( router: IRouter, @@ -44,37 +43,47 @@ export const updateTimelinesRoute = ( const frameworkRequest = await buildFrameworkRequest(context, security, request); const { timelineId, timeline, version } = request.body; const { templateTimelineId, templateTimelineVersion, timelineType } = timeline; - const isHandlingTemplateTimeline = timelineType === TimelineType.template; - const existTimeline = - timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; - const existTemplateTimeline = - templateTimelineId != null - ? await getTemplateTimeline(frameworkRequest, templateTimelineId) - : null; - const errorObj = checkIsFailureCases( - isHandlingTemplateTimeline, - version, - templateTimelineVersion ?? null, - existTimeline, - existTemplateTimeline - ); - if (errorObj != null) { - return siemResponse.error(errorObj); - } - const updatedTimeline = await createTimelines( - (frameworkRequest as unknown) as FrameworkRequest, - timeline, - timelineId, - version - ); - return response.ok({ - body: { - data: { - persistTimeline: updatedTimeline, - }, + const timelineStatus = new TimelinesStatus({ + timelineType: timelineType ?? TimelineType.default, + timelineInput: { + id: timelineId, + type: TimelineType.default, + version, }, + templateTimelineInput: { + id: templateTimelineId ?? null, + type: TimelineType.template, + version: templateTimelineVersion ?? null, + }, + frameworkRequest, }); + + await timelineStatus.setAvailableActions(); + + if (timelineStatus.isUpdatable) { + const updatedTimeline = await createTimelines( + (frameworkRequest as unknown) as FrameworkRequest, + timeline, + timelineId, + version + ); + return response.ok({ + body: { + data: { + persistTimeline: updatedTimeline, + }, + }, + }); + } else { + const error = timelineStatus.checkIsFailureCases(TimelineStatusActions.update); + return siemResponse.error( + error || { + statusCode: 405, + body: 'update timeline error', + } + ); + } } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts index 1036a74b74a03..6122f73e90933 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts @@ -5,11 +5,21 @@ */ import { set } from 'lodash/fp'; -import { SetupPlugins } from '../../../../plugin'; import { KibanaRequest } from '../../../../../../../../src/core/server'; import { RequestHandlerContext } from '../../../../../../../../target/types/core/server'; + +import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../../common/types/timeline'; +import { SetupPlugins } from '../../../../plugin'; + import { FrameworkRequest } from '../../../framework'; +import { TimelineInput } from './timeline_input'; +import { + checkIsCreateFailureCases, + checkIsUpdateFailureCases, + checkIsCreateViaImportFailureCases, +} from './failure_cases'; + export const buildFrameworkRequest = async ( context: RequestHandlerContext, security: SetupPlugins['security'], @@ -28,3 +38,132 @@ export const buildFrameworkRequest = async ( ) ); }; + +interface GivenTimelineInput { + id: string | null; + type: TimelineTypeLiteralWithNull; + version: string | number | null; +} + +export enum TimelineStatusActions { + create = 'create', + createViaImport = 'createViaImport', + update = 'update', + updateViaImport = 'updateViaImport', +} + +export type TimelineStatusAction = + | TimelineStatusActions.create + | TimelineStatusActions.createViaImport + | TimelineStatusActions.update + | TimelineStatusActions.updateViaImport; + +export class TimelinesStatus { + timelineInput: TimelineInput; + templateTimelineInput: TimelineInput; + isHandlingTemplateTimeline: boolean; + isCreatable: boolean; + isCreatableViaImport: boolean; + isUpdatable: boolean; + isUpdatableViaImport: boolean; + errorMessage: { body: string; statusCode: number } | null; + + constructor({ + timelineType = TimelineType.default, + timelineInput, + templateTimelineInput, + frameworkRequest, + }: { + timelineType: TimelineTypeLiteralWithNull; + timelineInput: GivenTimelineInput; + templateTimelineInput: GivenTimelineInput; + frameworkRequest: FrameworkRequest; + }) { + this.timelineInput = new TimelineInput({ + id: timelineInput.id, + type: timelineInput.type, + version: timelineInput.version, + frameworkRequest, + }); + + this.templateTimelineInput = new TimelineInput({ + id: templateTimelineInput.id, + type: templateTimelineInput.type, + version: templateTimelineInput.version, + frameworkRequest, + }); + + this.isHandlingTemplateTimeline = timelineType === TimelineType.template; + this.errorMessage = null; + + this.isCreatable = true; + this.isCreatableViaImport = false; + this.isUpdatable = true; + this.isUpdatableViaImport = false; + } + + private setIsCreatable() { + this.isCreatable = + (this.timelineInput.creatable && !this.isHandlingTemplateTimeline) || + (this.templateTimelineInput.creatable && + this.timelineInput.creatable && + this.isHandlingTemplateTimeline); + } + + private setIsCreatableViaImport() { + this.isCreatableViaImport = this.isCreatable; + } + + private setIsUpdatable() { + this.isUpdatable = + (this.timelineInput.updatable && !this.isHandlingTemplateTimeline) || + (this.templateTimelineInput.updatable && this.isHandlingTemplateTimeline); + } + + private setIsUpdatableViaImport() { + this.isUpdatableViaImport = + (this.timelineInput.allowUpdateViaImport && !this.isHandlingTemplateTimeline) || + (this.templateTimelineInput.allowUpdateViaImport && this.isHandlingTemplateTimeline); + } + + private getFailureChecker(action?: TimelineStatusAction) { + if (action === TimelineStatusActions.create) { + return checkIsCreateFailureCases; + } else if (action === TimelineStatusActions.createViaImport) { + return checkIsCreateViaImportFailureCases; + } else { + return checkIsUpdateFailureCases; + } + } + + public checkIsFailureCases(action?: TimelineStatusAction) { + const failureChecker = this.getFailureChecker(action); + this.errorMessage = failureChecker( + this.isHandlingTemplateTimeline, + this.timelineInput.version?.toString() ?? null, + this.templateTimelineInput.version != null && + typeof this.templateTimelineInput.version === 'string' + ? parseInt(this.templateTimelineInput.version, 10) + : this.templateTimelineInput.version, + this.timelineInput.data, + this.templateTimelineInput.data + ); + return this.errorMessage; + } + + getTimelines() { + return Promise.all([ + this.timelineInput.getTimelines(), + this.templateTimelineInput.getTimelines(), + ]); + } + + public async setAvailableActions() { + await this.getTimelines(); + + this.setIsCreatable(); + this.setIsCreatableViaImport(); + this.setIsUpdatable(); + this.setIsUpdatableViaImport(); + } +} diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts index 2c67a514cdf97..2af6260458fb1 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts @@ -12,6 +12,7 @@ import { FrameworkRequest } from '../../../framework'; import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline'; import { SavedNote } from '../../../../../common/types/timeline/note'; import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; + export const CREATE_TIMELINE_ERROR_MESSAGE = 'UPDATE timeline with POST is not allowed, please use PATCH instead'; export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = 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/failure_cases.ts similarity index 59% rename from x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts rename to x-pack/plugins/siem/server/lib/timeline/routes/utils/failure_cases.ts index a4efa676daddc..8d62e24030013 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/failure_cases.ts @@ -15,8 +15,12 @@ export const NO_MATCH_VERSION_ERROR_MESSAGE = export const NO_MATCH_ID_ERROR_MESSAGE = "Timeline id doesn't match with existing template timeline"; export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict'; +export const CREATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE timeline with POST is not allowed, please use PATCH instead'; +export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; -export const checkIsFailureCases = ( +export const checkIsUpdateFailureCases = ( isHandlingTemplateTimeline: boolean, version: string | null, templateTimelineVersion: number | null, @@ -24,11 +28,13 @@ export const checkIsFailureCases = ( existTemplateTimeline: TimelineSavedObject | null ) => { if (!isHandlingTemplateTimeline && existTimeline == null) { + // timeline !exists return { body: UPDATE_TIMELINE_ERROR_MESSAGE, statusCode: 405, }; } else if (isHandlingTemplateTimeline && existTemplateTimeline == null) { + // template timeline !exists // Throw error to create template timeline in patch return { body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, @@ -78,3 +84,53 @@ export const checkIsFailureCases = ( return null; } }; + +export const checkIsCreateFailureCases = ( + isHandlingTemplateTimeline: boolean, + version: string | null, + templateTimelineVersion: number | null, + existTimeline: TimelineSavedObject | null, + existTemplateTimeline: TimelineSavedObject | null +) => { + if (!isHandlingTemplateTimeline && existTimeline != null) { + return { + body: CREATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if (isHandlingTemplateTimeline && existTemplateTimeline != null) { + // Throw error to create template timeline in patch + return { + body: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else { + return null; + } +}; + +const getImportExistingTimelineError = (id: string, timelineType: string) => + `${timelineType}_id: "${id}" already exists`; + +export const checkIsCreateViaImportFailureCases = ( + isHandlingTemplateTimeline: boolean, + version: string | null, + templateTimelineVersion: number | null, + existTimeline: TimelineSavedObject | null, + existTemplateTimeline: TimelineSavedObject | null +) => { + const timelineType = isHandlingTemplateTimeline ? 'template_timeline' : 'timeline'; + if (!isHandlingTemplateTimeline && existTimeline != null) { + return { + body: getImportExistingTimelineError(existTimeline.savedObjectId, timelineType), + statusCode: 405, + }; + } else if (isHandlingTemplateTimeline && existTemplateTimeline != null) { + // Throw error to create template timeline in patch + return { + body: getImportExistingTimelineError(existTemplateTimeline.savedObjectId, timelineType), + statusCode: 405, + }; + } else { + return null; + } +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/timeline_input.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/timeline_input.ts new file mode 100644 index 0000000000000..20c0b476cb1c6 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/timeline_input.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + TimelineType, + TimelineTypeLiteralWithNull, + TimelineSavedObject, +} from '../../../../../common/types/timeline'; +import { getTimeline, getTemplateTimeline } from './create_timelines'; +import { FrameworkRequest } from '../../../framework'; + +export class TimelineInput { + id: string | null; + type: TimelineTypeLiteralWithNull; + version: string | number | null; + frameworkRequest: FrameworkRequest; + data: TimelineSavedObject | null; + exists: boolean; + creatable: boolean; + updatable: boolean; + allowUpdateViaImport: boolean; + versionConflict: boolean; + + constructor({ + id, + type = TimelineType.default, + version, + frameworkRequest, + }: { + id: string | null; + type: TimelineTypeLiteralWithNull; + version: string | number | null; + frameworkRequest: FrameworkRequest; + }) { + this.id = id; + this.type = type; + + this.version = version; + this.frameworkRequest = frameworkRequest; + this.data = null; + this.exists = false; + this.creatable = false; + this.updatable = false; + this.allowUpdateViaImport = false; + this.versionConflict = false; + } + + async getTimelines() { + this.data = + this.id != null + ? this.type === TimelineType.template + ? await getTemplateTimeline(this.frameworkRequest, this.id) + : await getTimeline(this.frameworkRequest, this.id) + : null; + + this.exists = this.id != null && this.data != null; + this.checkIfVersionConflict(); + this.creatable = !this.exists; + this.updatable = this.id != null && this.exists && !this.versionConflict; + this.allowUpdateViaImport = this.type === TimelineType.template && this.updatable; + } + + public checkIfVersionConflict() { + let isVersionConflict = false; + const existingVersion = + this.type === TimelineType.template ? this.data?.templateTimelineVersion : this.data?.version; + if (this.exists && this.version != null) { + isVersionConflict = !(this.version === existingVersion); + } else if (this.exists && this.version == null) { + isVersionConflict = true; + } + this.versionConflict = isVersionConflict; + } +} diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index 6d022ab42fa7b..4be4e04d46086 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -103,7 +103,7 @@ const getTimelineTypeFilter = (timelineType: string | null) => { : /** Show me every timeline whose timelineType is not "template". * which includes timelineType === 'default' and * those timelineType doesn't exists */ - `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template}`; + `not siem-ui-timeline.attributes.timelineType: ${TimelineType.template} and not siem-ui-timeline.attributes.timelineType: ${TimelineType.draft}`; }; export const getAllTimeline = async (