Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
This commit is contained in:
parent
5ce7267567
commit
8236569ffb
|
@ -246,6 +246,7 @@ export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/
|
|||
export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`;
|
||||
export const DETECTION_ENGINE_RULES_BULK_ACTION = `${DETECTION_ENGINE_RULES_URL}/_bulk_action`;
|
||||
|
||||
export const TIMELINE_RESOLVE_URL = '/api/timeline/resolve';
|
||||
export const TIMELINE_URL = '/api/timeline';
|
||||
export const TIMELINES_URL = '/api/timelines';
|
||||
export const TIMELINE_FAVORITE_URL = '/api/timeline/_favorite';
|
||||
|
|
|
@ -337,21 +337,6 @@ export const TimelineIdLiteralRt = runtimeTypes.union([
|
|||
|
||||
export type TimelineIdLiteral = runtimeTypes.TypeOf<typeof TimelineIdLiteralRt>;
|
||||
|
||||
/**
|
||||
* Timeline Saved object type with metadata
|
||||
*/
|
||||
|
||||
export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([
|
||||
runtimeTypes.type({
|
||||
id: runtimeTypes.string,
|
||||
attributes: SavedTimelineRuntimeType,
|
||||
version: runtimeTypes.string,
|
||||
}),
|
||||
runtimeTypes.partial({
|
||||
savedObjectId: runtimeTypes.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([
|
||||
SavedTimelineRuntimeType,
|
||||
runtimeTypes.type({
|
||||
|
@ -379,6 +364,33 @@ export const SingleTimelineResponseType = runtimeTypes.type({
|
|||
|
||||
export type SingleTimelineResponse = runtimeTypes.TypeOf<typeof SingleTimelineResponseType>;
|
||||
|
||||
/** Resolved Timeline Response */
|
||||
export const ResolvedTimelineSavedObjectToReturnObjectRuntimeType = runtimeTypes.intersection([
|
||||
runtimeTypes.type({
|
||||
timeline: TimelineSavedToReturnObjectRuntimeType,
|
||||
outcome: runtimeTypes.union([
|
||||
runtimeTypes.literal('exactMatch'),
|
||||
runtimeTypes.literal('aliasMatch'),
|
||||
runtimeTypes.literal('conflict'),
|
||||
]),
|
||||
}),
|
||||
runtimeTypes.partial({
|
||||
alias_target_id: runtimeTypes.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ResolvedTimelineWithOutcomeSavedObject = runtimeTypes.TypeOf<
|
||||
typeof ResolvedTimelineSavedObjectToReturnObjectRuntimeType
|
||||
>;
|
||||
|
||||
export const ResolvedSingleTimelineResponseType = runtimeTypes.type({
|
||||
data: ResolvedTimelineSavedObjectToReturnObjectRuntimeType,
|
||||
});
|
||||
|
||||
export type SingleTimelineResolveResponse = runtimeTypes.TypeOf<
|
||||
typeof ResolvedSingleTimelineResponseType
|
||||
>;
|
||||
|
||||
/**
|
||||
* All Timeline Saved object type with metadata
|
||||
*/
|
||||
|
|
|
@ -22,11 +22,13 @@ import {
|
|||
ResponseFavoriteTimeline,
|
||||
AllTimelinesResponse,
|
||||
SingleTimelineResponse,
|
||||
SingleTimelineResolveResponse,
|
||||
allTimelinesResponse,
|
||||
responseFavoriteTimeline,
|
||||
GetTimelinesArgs,
|
||||
SingleTimelineResponseType,
|
||||
TimelineType,
|
||||
ResolvedSingleTimelineResponseType,
|
||||
} from '../../../common/types/timeline';
|
||||
import {
|
||||
TIMELINE_URL,
|
||||
|
@ -34,6 +36,7 @@ import {
|
|||
TIMELINE_IMPORT_URL,
|
||||
TIMELINE_EXPORT_URL,
|
||||
TIMELINE_PREPACKAGED_URL,
|
||||
TIMELINE_RESOLVE_URL,
|
||||
TIMELINES_URL,
|
||||
TIMELINE_FAVORITE_URL,
|
||||
} from '../../../common/constants';
|
||||
|
@ -71,6 +74,12 @@ const decodeSingleTimelineResponse = (respTimeline?: SingleTimelineResponse) =>
|
|||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
const decodeResolvedSingleTimelineResponse = (respTimeline?: SingleTimelineResolveResponse) =>
|
||||
pipe(
|
||||
ResolvedSingleTimelineResponseType.decode(respTimeline),
|
||||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
const decodeAllTimelinesResponse = (respTimeline: AllTimelinesResponse) =>
|
||||
pipe(
|
||||
allTimelinesResponse.decode(respTimeline),
|
||||
|
@ -305,6 +314,19 @@ export const getTimeline = async (id: string) => {
|
|||
return decodeSingleTimelineResponse(response);
|
||||
};
|
||||
|
||||
export const resolveTimeline = async (id: string) => {
|
||||
const response = await KibanaServices.get().http.get<SingleTimelineResolveResponse>(
|
||||
TIMELINE_RESOLVE_URL,
|
||||
{
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return decodeResolvedSingleTimelineResponse(response);
|
||||
};
|
||||
|
||||
export const getTimelineTemplate = async (templateTimelineId: string) => {
|
||||
const response = await KibanaServices.get().http.get<SingleTimelineResponse>(TIMELINE_URL, {
|
||||
query: {
|
||||
|
@ -315,6 +337,19 @@ export const getTimelineTemplate = async (templateTimelineId: string) => {
|
|||
return decodeSingleTimelineResponse(response);
|
||||
};
|
||||
|
||||
export const getResolvedTimelineTemplate = async (templateTimelineId: string) => {
|
||||
const response = await KibanaServices.get().http.get<SingleTimelineResolveResponse>(
|
||||
TIMELINE_RESOLVE_URL,
|
||||
{
|
||||
query: {
|
||||
template_timeline_id: templateTimelineId,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return decodeResolvedSingleTimelineResponse(response);
|
||||
};
|
||||
|
||||
export const getAllTimelines = async (args: GetTimelinesArgs, abortSignal: AbortSignal) => {
|
||||
const response = await KibanaServices.get().http.fetch<AllTimelinesResponse>(TIMELINES_URL, {
|
||||
method: 'GET',
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
|
||||
import { ResolvedTimelineWithOutcomeSavedObject } from './../../../../common/types/timeline/index';
|
||||
|
||||
export const mockResolvedSavedObject = {
|
||||
saved_object: {
|
||||
id: '760d3d20-2142-11ec-a46f-051cb8e3154c',
|
||||
type: 'siem-ui-timeline',
|
||||
namespaces: ['default'],
|
||||
updated_at: '2021-09-29T16:29:48.478Z',
|
||||
version: 'WzYxNzc0LDFd',
|
||||
attributes: {
|
||||
columns: [],
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
eventType: 'all',
|
||||
filters: [],
|
||||
kqlMode: 'filter',
|
||||
timelineType: 'default',
|
||||
kqlQuery: {
|
||||
filterQuery: null,
|
||||
},
|
||||
title: 'Test Timeline',
|
||||
sort: [
|
||||
{
|
||||
columnType: 'date',
|
||||
sortDirection: 'desc',
|
||||
columnId: '@timestamp',
|
||||
},
|
||||
],
|
||||
status: 'active',
|
||||
created: 1632932987378,
|
||||
createdBy: 'tester',
|
||||
updated: 1632932988422,
|
||||
updatedBy: 'tester',
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
excludedRowRendererIds: [],
|
||||
dateRange: {
|
||||
start: '2021-09-29T04:00:00.000Z',
|
||||
end: '2021-09-30T03:59:59.999Z',
|
||||
},
|
||||
indexNames: [],
|
||||
eqlOptions: {
|
||||
tiebreakerField: '',
|
||||
size: 100,
|
||||
query: '',
|
||||
eventCategoryField: 'event.category',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
migrationVersion: {
|
||||
'siem-ui-timeline': '7.16.0',
|
||||
},
|
||||
coreMigrationVersion: '8.0.0',
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: 'new-saved-object-id',
|
||||
};
|
||||
|
||||
export const mockResolvedTimeline = {
|
||||
savedObjectId: '760d3d20-2142-11ec-a46f-051cb8e3154c',
|
||||
version: 'WzY1NDcxLDFd',
|
||||
columns: [],
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
eventType: 'all',
|
||||
filters: [],
|
||||
kqlMode: 'filter',
|
||||
timelineType: TimelineType.default,
|
||||
kqlQuery: { filterQuery: null },
|
||||
title: 'Test Timeline',
|
||||
sort: [
|
||||
{
|
||||
columnType: 'date',
|
||||
sortDirection: 'desc',
|
||||
columnId: '@timestamp',
|
||||
},
|
||||
],
|
||||
status: TimelineStatus.active,
|
||||
created: 1632932987378,
|
||||
createdBy: 'tester',
|
||||
updated: 1632932988422,
|
||||
updatedBy: 'tester',
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
excludedRowRendererIds: [],
|
||||
dateRange: {
|
||||
start: '2021-09-29T04:00:00.000Z',
|
||||
end: '2021-09-30T03:59:59.999Z',
|
||||
},
|
||||
indexNames: [],
|
||||
eqlOptions: {
|
||||
tiebreakerField: '',
|
||||
size: 100,
|
||||
query: '',
|
||||
eventCategoryField: 'event.category',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
savedQueryId: null,
|
||||
};
|
||||
|
||||
export const mockPopulatedTimeline = {
|
||||
...mockResolvedTimeline,
|
||||
eventIdToNoteIds: [],
|
||||
favorite: [],
|
||||
noteIds: [],
|
||||
notes: [],
|
||||
pinnedEventIds: [],
|
||||
pinnedEventsSaveObject: [],
|
||||
};
|
||||
|
||||
export const mockResolveTimelineResponse: ResolvedTimelineWithOutcomeSavedObject = {
|
||||
timeline: mockPopulatedTimeline,
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: 'new-saved-object-id',
|
||||
};
|
|
@ -12,3 +12,4 @@ export { getTimelinesRoute } from './get_timelines';
|
|||
export { importTimelinesRoute } from './import_timelines';
|
||||
export { patchTimelinesRoute } from './patch_timelines';
|
||||
export { persistFavoriteRoute } from './persist_favorite';
|
||||
export { resolveTimelineRoute } from './resolve_timeline';
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../../types';
|
||||
|
||||
import { TIMELINE_RESOLVE_URL } from '../../../../../../common/constants';
|
||||
|
||||
import { ConfigType } from '../../../../..';
|
||||
import { SetupPlugins } from '../../../../../plugin';
|
||||
import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation';
|
||||
|
||||
import { buildSiemResponse } from '../../../../detection_engine/routes/utils';
|
||||
|
||||
import { buildFrameworkRequest } from '../../../utils/common';
|
||||
import { getTimelineQuerySchema } from '../../../schemas/timelines';
|
||||
import { getTimelineTemplateOrNull, resolveTimelineOrNull } from '../../../saved_object/timelines';
|
||||
|
||||
export const resolveTimelineRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: TIMELINE_RESOLVE_URL,
|
||||
validate: {
|
||||
query: buildRouteValidationWithExcess(getTimelineQuerySchema),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const frameworkRequest = await buildFrameworkRequest(context, security, request);
|
||||
const query = request.query ?? {};
|
||||
const { template_timeline_id: templateTimelineId, id } = query;
|
||||
|
||||
let res = null;
|
||||
|
||||
if (templateTimelineId != null && id == null) {
|
||||
// Template timelineId is not a SO id, so it does not need to be updated to use resolve
|
||||
res = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId);
|
||||
} else if (templateTimelineId == null && id != null) {
|
||||
// In the event the objectId is defined, run the resolve call
|
||||
res = await resolveTimelineOrNull(frameworkRequest, id);
|
||||
} else {
|
||||
throw new Error('please provide id or template_timeline_id');
|
||||
}
|
||||
|
||||
return response.ok({ body: res ? { data: res } : {} });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -8,11 +8,24 @@
|
|||
import { FrameworkRequest } from '../../../framework';
|
||||
import { mockGetTimelineValue, mockSavedObject } from '../../__mocks__/import_timelines';
|
||||
|
||||
import { convertStringToBase64, getExistingPrepackagedTimelines, getAllTimeline } from '.';
|
||||
import {
|
||||
convertStringToBase64,
|
||||
getExistingPrepackagedTimelines,
|
||||
getAllTimeline,
|
||||
resolveTimelineOrNull,
|
||||
} from '.';
|
||||
import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline';
|
||||
import { getNotesByTimelineId } from '../notes/saved_object';
|
||||
import { getAllPinnedEventsByTimelineId } from '../pinned_events';
|
||||
import { AllTimelinesResponse } from '../../../../../common/types/timeline';
|
||||
import {
|
||||
AllTimelinesResponse,
|
||||
ResolvedTimelineWithOutcomeSavedObject,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import {
|
||||
mockResolvedSavedObject,
|
||||
mockResolvedTimeline,
|
||||
mockResolveTimelineResponse,
|
||||
} from '../../__mocks__/resolve_timeline';
|
||||
|
||||
jest.mock('./convert_saved_object_to_savedtimeline', () => ({
|
||||
convertSavedObjectToSavedTimeline: jest.fn(),
|
||||
|
@ -151,7 +164,7 @@ describe('saved_object', () => {
|
|||
(getAllPinnedEventsByTimelineId as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
test('should send correct options if no filters applys', async () => {
|
||||
test('should send correct options if no filters applies', async () => {
|
||||
expect(mockFindSavedObject.mock.calls[0][0]).toEqual({
|
||||
filter: 'not siem-ui-timeline.attributes.status: draft',
|
||||
page: pageInfo.pageIndex,
|
||||
|
@ -226,7 +239,7 @@ describe('saved_object', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('should retuen correct result', async () => {
|
||||
test('should return correct result', async () => {
|
||||
expect(result).toEqual({
|
||||
totalCount: 1,
|
||||
customTemplateTimelineCount: 0,
|
||||
|
@ -248,4 +261,52 @@ describe('saved_object', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTimelineOrNull', () => {
|
||||
let mockResolveSavedObject: jest.Mock;
|
||||
let mockRequest: FrameworkRequest;
|
||||
let result: ResolvedTimelineWithOutcomeSavedObject | null = null;
|
||||
beforeEach(async () => {
|
||||
(convertSavedObjectToSavedTimeline as jest.Mock).mockReturnValue(mockResolvedTimeline);
|
||||
mockResolveSavedObject = jest.fn().mockReturnValue(mockResolvedSavedObject);
|
||||
mockRequest = {
|
||||
user: {
|
||||
username: 'username',
|
||||
},
|
||||
context: {
|
||||
core: {
|
||||
savedObjects: {
|
||||
client: {
|
||||
resolve: mockResolveSavedObject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as FrameworkRequest;
|
||||
|
||||
result = await resolveTimelineOrNull(mockRequest, '760d3d20-2142-11ec-a46f-051cb8e3154c');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockResolveSavedObject.mockClear();
|
||||
(getNotesByTimelineId as jest.Mock).mockClear();
|
||||
(getAllPinnedEventsByTimelineId as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
test('should call getNotesByTimelineId', async () => {
|
||||
expect((getNotesByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(
|
||||
mockResolvedSavedObject.saved_object.id
|
||||
);
|
||||
});
|
||||
|
||||
test('should call getAllPinnedEventsByTimelineId', async () => {
|
||||
expect((getAllPinnedEventsByTimelineId as jest.Mock).mock.calls[0][1]).toEqual(
|
||||
mockResolvedSavedObject.saved_object.id
|
||||
);
|
||||
});
|
||||
|
||||
test('should return the timeline with resolve attributes', async () => {
|
||||
expect(result).toEqual(mockResolveTimelineResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
TimelineStatus,
|
||||
TimelineResult,
|
||||
TimelineWithoutExternalRefs,
|
||||
ResolvedTimelineWithOutcomeSavedObject,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { FrameworkRequest } from '../../../framework';
|
||||
import * as note from '../notes/saved_object';
|
||||
|
@ -52,49 +53,6 @@ export interface ResponseTemplateTimeline {
|
|||
templateTimeline: TimelineResult;
|
||||
}
|
||||
|
||||
export interface Timeline {
|
||||
getTimeline: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string,
|
||||
timelineType?: TimelineTypeLiteralWithNull
|
||||
) => Promise<TimelineSavedObject>;
|
||||
|
||||
getAllTimeline: (
|
||||
request: FrameworkRequest,
|
||||
onlyUserFavorite: boolean | null,
|
||||
pageInfo: PageInfoTimeline,
|
||||
search: string | null,
|
||||
sort: SortTimeline | null,
|
||||
status: TimelineStatusLiteralWithNull,
|
||||
timelineType: TimelineTypeLiteralWithNull
|
||||
) => Promise<AllTimelinesResponse>;
|
||||
|
||||
persistFavorite: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null,
|
||||
templateTimelineId: string | null,
|
||||
templateTimelineVersion: number | null,
|
||||
timelineType: TimelineType
|
||||
) => Promise<ResponseFavoriteTimeline>;
|
||||
|
||||
persistTimeline: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null,
|
||||
version: string | null,
|
||||
timeline: SavedTimeline,
|
||||
isImmutable?: boolean
|
||||
) => Promise<ResponseTimeline>;
|
||||
|
||||
deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise<void>;
|
||||
convertStringToBase64: (text: string) => string;
|
||||
timelineWithReduxProperties: (
|
||||
notes: NoteSavedObject[],
|
||||
pinnedEvents: PinnedEventSavedObject[],
|
||||
timeline: TimelineSavedObject,
|
||||
userName: string
|
||||
) => TimelineSavedObject;
|
||||
}
|
||||
|
||||
export const getTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string,
|
||||
|
@ -132,6 +90,18 @@ export const getTimelineOrNull = async (
|
|||
return timeline;
|
||||
};
|
||||
|
||||
export const resolveTimelineOrNull = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
savedObjectId: string
|
||||
): Promise<ResolvedTimelineWithOutcomeSavedObject | null> => {
|
||||
let resolvedTimeline = null;
|
||||
try {
|
||||
resolvedTimeline = await resolveSavedTimeline(frameworkRequest, savedObjectId);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
return resolvedTimeline;
|
||||
};
|
||||
|
||||
export const getTimelineByTemplateTimelineId = async (
|
||||
request: FrameworkRequest,
|
||||
templateTimelineId: string
|
||||
|
@ -584,6 +554,44 @@ export const deleteTimeline = async (request: FrameworkRequest, timelineIds: str
|
|||
);
|
||||
};
|
||||
|
||||
const resolveBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const { saved_object: savedObject, ...resolveAttributes } =
|
||||
await savedObjectsClient.resolve<TimelineWithoutExternalRefs>(
|
||||
timelineSavedObjectType,
|
||||
timelineId
|
||||
);
|
||||
|
||||
const populatedTimeline = timelineFieldsMigrator.populateFieldsFromReferences(savedObject);
|
||||
|
||||
return {
|
||||
resolvedTimelineSavedObject: convertSavedObjectToSavedTimeline(populatedTimeline),
|
||||
...resolveAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveSavedTimeline = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
|
||||
const { resolvedTimelineSavedObject, ...resolveAttributes } = await resolveBasicSavedTimeline(
|
||||
request,
|
||||
timelineId
|
||||
);
|
||||
|
||||
const timelineWithNotesAndPinnedEvents = await Promise.all([
|
||||
note.getNotesByTimelineId(request, resolvedTimelineSavedObject.savedObjectId),
|
||||
pinnedEvent.getAllPinnedEventsByTimelineId(request, resolvedTimelineSavedObject.savedObjectId),
|
||||
resolvedTimelineSavedObject,
|
||||
]);
|
||||
|
||||
const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents;
|
||||
|
||||
return {
|
||||
timeline: timelineWithReduxProperties(notes, pinnedEvents, timeline, userName),
|
||||
...resolveAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get<TimelineWithoutExternalRefs>(
|
||||
|
@ -646,13 +654,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje
|
|||
|
||||
export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64');
|
||||
|
||||
// we have to use any here because the SavedObjectAttributes interface is like below
|
||||
// export interface SavedObjectAttributes {
|
||||
// [key: string]: SavedObjectAttributes | string | number | boolean | null;
|
||||
// }
|
||||
// then this interface does not allow types without index signature
|
||||
// this is limiting us with our type for now so the easy way was to use any
|
||||
|
||||
export const timelineWithReduxProperties = (
|
||||
notes: NoteSavedObject[],
|
||||
pinnedEvents: PinnedEventSavedObject[],
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
importTimelinesRoute,
|
||||
patchTimelinesRoute,
|
||||
persistFavoriteRoute,
|
||||
resolveTimelineRoute,
|
||||
} from '../lib/timeline/routes/timelines';
|
||||
import { getDraftTimelinesRoute } from '../lib/timeline/routes/draft_timelines/get_draft_timelines';
|
||||
import { cleanDraftTimelinesRoute } from '../lib/timeline/routes/draft_timelines/clean_draft_timelines';
|
||||
|
@ -99,6 +100,7 @@ export const initRoutes = (
|
|||
exportTimelinesRoute(router, config, security);
|
||||
getDraftTimelinesRoute(router, config, security);
|
||||
getTimelineRoute(router, config, security);
|
||||
resolveTimelineRoute(router, config, security);
|
||||
getTimelinesRoute(router, config, security);
|
||||
cleanDraftTimelinesRoute(router, config, security);
|
||||
deleteTimelinesRoute(router, config, security);
|
||||
|
|
|
@ -16,44 +16,121 @@ import { createBasicTimeline, createBasicTimelineTemplate } from './saved_object
|
|||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('Timeline', () => {
|
||||
it('Make sure that we get Timeline data', async () => {
|
||||
const titleToSaved = 'hello timeline';
|
||||
await createBasicTimeline(supertest, titleToSaved);
|
||||
describe('timelines', () => {
|
||||
it('Make sure that we get Timeline data', async () => {
|
||||
const titleToSaved = 'hello timeline';
|
||||
await createBasicTimeline(supertest, titleToSaved);
|
||||
|
||||
const resp = await supertest.get('/api/timelines').set('kbn-xsrf', 'true');
|
||||
const resp = await supertest.get('/api/timelines').set('kbn-xsrf', 'true');
|
||||
|
||||
const timelines = resp.body.timeline;
|
||||
const timelines = resp.body.timeline;
|
||||
|
||||
expect(timelines.length).to.greaterThan(0);
|
||||
expect(timelines.length).to.greaterThan(0);
|
||||
});
|
||||
|
||||
it('Make sure that pagination is working in Timeline query', async () => {
|
||||
const titleToSaved = 'hello timeline';
|
||||
await createBasicTimeline(supertest, titleToSaved);
|
||||
|
||||
const resp = await supertest
|
||||
.get('/api/timelines?page_size=1&page_index=1')
|
||||
.set('kbn-xsrf', 'true');
|
||||
|
||||
const timelines = resp.body.timeline;
|
||||
|
||||
expect(timelines.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('Make sure that we get Timeline template data', async () => {
|
||||
const titleToSaved = 'hello timeline template';
|
||||
await createBasicTimelineTemplate(supertest, titleToSaved);
|
||||
|
||||
const resp = await supertest
|
||||
.get('/api/timelines?timeline_type=template')
|
||||
.set('kbn-xsrf', 'true');
|
||||
|
||||
const templates: SavedTimeline[] = resp.body.timeline;
|
||||
|
||||
expect(templates.length).to.greaterThan(0);
|
||||
expect(templates.filter((t) => t.timelineType === TimelineType.default).length).to.equal(0);
|
||||
});
|
||||
});
|
||||
describe('resolve timeline', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
|
||||
);
|
||||
});
|
||||
|
||||
it('Make sure that pagination is working in Timeline query', async () => {
|
||||
const titleToSaved = 'hello timeline';
|
||||
await createBasicTimeline(supertest, titleToSaved);
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
|
||||
);
|
||||
});
|
||||
|
||||
const resp = await supertest
|
||||
.get('/api/timelines?page_size=1&page_index=1')
|
||||
.set('kbn-xsrf', 'true');
|
||||
it('should return outcome exactMatch when the id is unchanged', async () => {
|
||||
const resp = await supertest
|
||||
.get('/api/timeline/resolve')
|
||||
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' });
|
||||
|
||||
const timelines = resp.body.timeline;
|
||||
expect(resp.body.data.outcome).to.be('exactMatch');
|
||||
expect(resp.body.data.alias_target_id).to.be(undefined);
|
||||
expect(resp.body.data.timeline.title).to.be('Awesome Timeline');
|
||||
});
|
||||
|
||||
expect(timelines.length).to.equal(1);
|
||||
});
|
||||
describe('notes', () => {
|
||||
it('should return notes with eventId', async () => {
|
||||
const resp = await supertest
|
||||
.get('/api/timeline/resolve')
|
||||
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
|
||||
|
||||
it('Make sure that we get Timeline template data', async () => {
|
||||
const titleToSaved = 'hello timeline template';
|
||||
await createBasicTimelineTemplate(supertest, titleToSaved);
|
||||
expect(resp.body.data.timeline.notes[0].eventId).to.be('Edo00XsBEVtyvU-8LGNe');
|
||||
});
|
||||
|
||||
const resp = await supertest
|
||||
.get('/api/timelines?timeline_type=template')
|
||||
.set('kbn-xsrf', 'true');
|
||||
it('should return notes with the timelineId matching request id', async () => {
|
||||
const resp = await supertest
|
||||
.get('/api/timeline/resolve')
|
||||
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
|
||||
|
||||
const templates: SavedTimeline[] = resp.body.timeline;
|
||||
expect(resp.body.data.timeline.notes[0].timelineId).to.be(
|
||||
'6484cc90-126e-11ec-83d2-db1096c73738'
|
||||
);
|
||||
expect(resp.body.data.timeline.notes[1].timelineId).to.be(
|
||||
'6484cc90-126e-11ec-83d2-db1096c73738'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(templates.length).to.greaterThan(0);
|
||||
expect(templates.filter((t) => t.timelineType === TimelineType.default).length).to.equal(0);
|
||||
describe('pinned events', () => {
|
||||
it('should pinned events with eventId', async () => {
|
||||
const resp = await supertest
|
||||
.get('/api/timeline/resolve')
|
||||
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
|
||||
|
||||
expect(resp.body.data.timeline.pinnedEventsSaveObject[0].eventId).to.be(
|
||||
'DNo00XsBEVtyvU-8LGNe'
|
||||
);
|
||||
expect(resp.body.data.timeline.pinnedEventsSaveObject[1].eventId).to.be(
|
||||
'Edo00XsBEVtyvU-8LGNe'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return pinned events with the timelineId matching request id', async () => {
|
||||
const resp = await supertest
|
||||
.get('/api/timeline/resolve')
|
||||
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
|
||||
|
||||
expect(resp.body.data.timeline.pinnedEventsSaveObject[0].timelineId).to.be(
|
||||
'6484cc90-126e-11ec-83d2-db1096c73738'
|
||||
);
|
||||
expect(resp.body.data.timeline.pinnedEventsSaveObject[1].timelineId).to.be(
|
||||
'6484cc90-126e-11ec-83d2-db1096c73738'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue