mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SIEM] Add api integration test + fix bug to default value in timeline (#39577)
* bug when persited timeline does not have all the data wanted on the UI * add api integration testing for timelien * review I * update snapshot * again update snapshots for stat_item
This commit is contained in:
parent
4b49db591d
commit
d74f4f7f9f
14 changed files with 676 additions and 88 deletions
|
@ -8965,12 +8965,6 @@
|
|||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
|
||||
|
|
|
@ -1978,8 +1978,6 @@ export interface PersistNoteMutationArgs {
|
|||
}
|
||||
export interface DeleteNoteMutationArgs {
|
||||
id: string[];
|
||||
|
||||
version?: string | null;
|
||||
}
|
||||
export interface DeleteNoteByTimelineIdMutationArgs {
|
||||
timelineId: string;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
||||
|
||||
import { KqlMode, TimelineModel } from './model';
|
||||
import { TimelineResult } from '../../graphql/types';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline');
|
||||
|
||||
|
@ -76,7 +77,7 @@ export const updateTimeline = actionCreator<{
|
|||
|
||||
export const addTimeline = actionCreator<{
|
||||
id: string;
|
||||
timeline: TimelineModel;
|
||||
timeline: TimelineResult;
|
||||
}>('ADD_TIMELINE');
|
||||
|
||||
export const startTimelineSaving = actionCreator<{
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp';
|
||||
import { getOr, omit, uniq, isEmpty, isEqualWith, defaultsDeep, pickBy, isNil } from 'lodash/fp';
|
||||
|
||||
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
|
||||
import { getColumnWidthFromType } from '../../components/timeline/body/helpers';
|
||||
|
@ -17,6 +17,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../model';
|
|||
|
||||
import { KqlMode, timelineDefaults, TimelineModel } from './model';
|
||||
import { TimelineById, TimelineState } from './types';
|
||||
import { TimelineResult } from '../../graphql/types';
|
||||
|
||||
const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference
|
||||
|
||||
|
@ -102,12 +103,31 @@ export const addTimelineNoteToEvent = ({
|
|||
};
|
||||
};
|
||||
|
||||
interface AddTimelineParams {
|
||||
id: string;
|
||||
timeline: TimelineResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a saved object timeline to the store
|
||||
* and default the value to what need to be if values are null
|
||||
*/
|
||||
export const addTimelineToStore = ({ id, timeline }: AddTimelineParams): TimelineById => ({
|
||||
// TODO: revisit this when we support multiple timelines
|
||||
[id]: {
|
||||
...defaultsDeep(timelineDefaults, pickBy(v => !isNil(v), timeline)),
|
||||
id: timeline.savedObjectId || '',
|
||||
show: true,
|
||||
},
|
||||
});
|
||||
|
||||
interface AddNewTimelineParams {
|
||||
columns: ColumnHeader[];
|
||||
id: string;
|
||||
show?: boolean;
|
||||
timelineById: TimelineById;
|
||||
}
|
||||
|
||||
/** Adds a new `Timeline` to the provided collection of `TimelineById` */
|
||||
export const addNewTimeline = ({
|
||||
columns,
|
||||
|
|
|
@ -24,6 +24,7 @@ import { defaultHeaders } from '../../mock';
|
|||
import {
|
||||
addNewTimeline,
|
||||
addTimelineProvider,
|
||||
addTimelineToStore,
|
||||
applyDeltaToTimelineColumnWidth,
|
||||
removeTimelineColumn,
|
||||
removeTimelineProvider,
|
||||
|
@ -97,6 +98,99 @@ const timelineByIdMock: TimelineById = {
|
|||
const columnsMock: ColumnHeader[] = [defaultHeaders[0], defaultHeaders[1], defaultHeaders[2]];
|
||||
|
||||
describe('Timeline', () => {
|
||||
describe('#add saved object Timeline to store ', () => {
|
||||
test('should return a timelineModel with default value and not just a timelineResult ', () => {
|
||||
const update = addTimelineToStore({
|
||||
id: 'foo',
|
||||
timeline: {
|
||||
savedObjectId: 'superUniqueId',
|
||||
title: 'saved object timeline',
|
||||
version: 'doNotForgetVersion',
|
||||
},
|
||||
});
|
||||
|
||||
expect(update).toEqual({
|
||||
foo: {
|
||||
columns: [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: '@timestamp',
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'message',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'event.category',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'event.action',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'host.name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'source.ip',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'destination.ip',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'user.name',
|
||||
width: 180,
|
||||
},
|
||||
],
|
||||
dataProviders: [],
|
||||
dateRange: {
|
||||
end: 0,
|
||||
start: 0,
|
||||
},
|
||||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
id: 'superUniqueId',
|
||||
isFavorite: false,
|
||||
isLive: false,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
itemsPerPage: 25,
|
||||
itemsPerPageOptions: [10, 25, 50, 100],
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: {
|
||||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
savedObjectId: 'superUniqueId',
|
||||
show: true,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
},
|
||||
title: 'saved object timeline',
|
||||
version: 'doNotForgetVersion',
|
||||
width: 1100,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addNewTimeline', () => {
|
||||
test('should return a new reference and not the same reference', () => {
|
||||
const update = addNewTimeline({
|
||||
|
|
|
@ -3,10 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
|
||||
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers';
|
||||
import {
|
||||
addTimeline,
|
||||
addHistory,
|
||||
|
@ -53,6 +51,7 @@ import {
|
|||
addTimelineNote,
|
||||
addTimelineNoteToEvent,
|
||||
addTimelineProvider,
|
||||
addTimelineToStore,
|
||||
applyDeltaToCurrentWidth,
|
||||
applyDeltaToTimelineColumnWidth,
|
||||
applyKqlFilterQueryDraft,
|
||||
|
@ -96,28 +95,7 @@ export const initialTimelineState: TimelineState = {
|
|||
export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
||||
.case(addTimeline, (state, { id, timeline }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
// As right now, We are not managing multiple timeline
|
||||
// for now simplification, we do not need the line below
|
||||
// ...state.timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isLive: false,
|
||||
isLoading: true,
|
||||
itemsPerPage: 25,
|
||||
itemsPerPageOptions: [10, 25, 50, 100],
|
||||
id: timeline.savedObjectId || '',
|
||||
dateRange: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
show: true,
|
||||
width: DEFAULT_TIMELINE_WIDTH,
|
||||
isSaving: false,
|
||||
},
|
||||
},
|
||||
timelineById: addTimelineToStore({ id, timeline }),
|
||||
}))
|
||||
.case(createTimeline, (state, { id, show, columns }) => ({
|
||||
...state,
|
||||
|
|
|
@ -75,7 +75,7 @@ export const noteSchema = gql`
|
|||
extend type Mutation {
|
||||
"Persists a note"
|
||||
persistNote(noteId: ID, version: String, note: NoteInput!): ResponseNote!
|
||||
deleteNote(id: [ID!]!, version: String):Boolean
|
||||
deleteNote(id: [ID!]!):Boolean
|
||||
deleteNoteByTimelineId(timelineId: ID!, version: String):Boolean
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -2007,8 +2007,6 @@ export interface PersistNoteMutationArgs {
|
|||
}
|
||||
export interface DeleteNoteMutationArgs {
|
||||
id: string[];
|
||||
|
||||
version?: string | null;
|
||||
}
|
||||
export interface DeleteNoteByTimelineIdMutationArgs {
|
||||
timelineId: string;
|
||||
|
@ -7116,8 +7114,6 @@ export namespace MutationResolvers {
|
|||
> = Resolver<R, Parent, Context, DeleteNoteArgs>;
|
||||
export interface DeleteNoteArgs {
|
||||
id: string[];
|
||||
|
||||
version?: string | null;
|
||||
}
|
||||
|
||||
export type DeleteNoteByTimelineIdResolver<
|
||||
|
|
|
@ -104,21 +104,24 @@ export class Note {
|
|||
version: string | null,
|
||||
note: SavedNote
|
||||
): Promise<ResponseNote> {
|
||||
let timelineVersionSavedObject = null;
|
||||
try {
|
||||
if (note.timelineId == null) {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
|
||||
)
|
||||
);
|
||||
note.timelineId = timelineResult.savedObjectId;
|
||||
timelineVersionSavedObject = timelineResult.version;
|
||||
}
|
||||
if (noteId == null) {
|
||||
const timelineVersionSavedObject =
|
||||
note.timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
|
||||
)
|
||||
);
|
||||
note.timelineId = timelineResult.savedObjectId;
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
|
||||
// Create new note
|
||||
return {
|
||||
code: 200,
|
||||
|
@ -134,6 +137,7 @@ export class Note {
|
|||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Update new note
|
||||
return {
|
||||
code: 200,
|
||||
|
|
|
@ -97,46 +97,52 @@ export class PinnedEvent {
|
|||
eventId: string,
|
||||
timelineId: string | null
|
||||
): Promise<PinnedEventSavedObject | null> {
|
||||
let timelineVersionSavedObject = null;
|
||||
try {
|
||||
if (timelineId == null) {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
|
||||
)
|
||||
);
|
||||
timelineId = timelineResult.savedObjectId;
|
||||
timelineVersionSavedObject = timelineResult.version;
|
||||
}
|
||||
if (pinnedEventId == null) {
|
||||
const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId);
|
||||
const isPinnedAlreadyExisting = allPinnedEventId.filter(
|
||||
pinnedEvent => pinnedEvent.eventId === eventId
|
||||
);
|
||||
if (isPinnedAlreadyExisting.length === 0) {
|
||||
const savedPinnedEvent: SavedPinnedEvent = {
|
||||
eventId,
|
||||
timelineId,
|
||||
};
|
||||
// create Pinned Event on Timeline
|
||||
return convertSavedObjectToSavedPinnedEvent(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
pinnedEventSavedObjectType,
|
||||
pickSavedPinnedEvent(
|
||||
pinnedEventId,
|
||||
savedPinnedEvent,
|
||||
request[internalFrameworkRequest].auth || null
|
||||
)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
const timelineVersionSavedObject =
|
||||
timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
|
||||
)
|
||||
);
|
||||
timelineId = timelineResult.savedObjectId;
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
|
||||
if (timelineId != null) {
|
||||
const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId);
|
||||
const isPinnedAlreadyExisting = allPinnedEventId.filter(
|
||||
pinnedEvent => pinnedEvent.eventId === eventId
|
||||
);
|
||||
if (isPinnedAlreadyExisting.length === 0) {
|
||||
const savedPinnedEvent: SavedPinnedEvent = {
|
||||
eventId,
|
||||
timelineId,
|
||||
};
|
||||
// create Pinned Event on Timeline
|
||||
return convertSavedObjectToSavedPinnedEvent(
|
||||
await this.libs.savedObjects
|
||||
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
|
||||
.create(
|
||||
pinnedEventSavedObjectType,
|
||||
pickSavedPinnedEvent(
|
||||
pinnedEventId,
|
||||
savedPinnedEvent,
|
||||
request[internalFrameworkRequest].auth || null
|
||||
)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
);
|
||||
}
|
||||
return isPinnedAlreadyExisting[0];
|
||||
}
|
||||
return isPinnedAlreadyExisting[0];
|
||||
throw new Error('You can NOT pinned event without a timelineID');
|
||||
}
|
||||
// Delete Pinned Event on Timeline
|
||||
await this.deletePinnedEventOnTimeline(request, [pinnedEventId]);
|
||||
|
|
|
@ -15,6 +15,9 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./network_dns'));
|
||||
loadTestFile(require.resolve('./network_top_n_flow'));
|
||||
loadTestFile(require.resolve('./overview_host'));
|
||||
loadTestFile(require.resolve('./saved_objects/notes'));
|
||||
loadTestFile(require.resolve('./saved_objects/pinned_events'));
|
||||
loadTestFile(require.resolve('./saved_objects/timeline'));
|
||||
loadTestFile(require.resolve('./sources'));
|
||||
loadTestFile(require.resolve('./overview_network'));
|
||||
loadTestFile(require.resolve('./timeline'));
|
||||
|
|
106
x-pack/test/api_integration/apis/siem/saved_objects/notes.ts
Normal file
106
x-pack/test/api_integration/apis/siem/saved_objects/notes.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { persistTimelineNoteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query';
|
||||
|
||||
import { KbnTestProvider } from '../types';
|
||||
|
||||
const notesPersistenceTests: KbnTestProvider = ({ getService }) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('siemGraphQLClient');
|
||||
|
||||
describe('Note - Saved Objects', () => {
|
||||
beforeEach(() => esArchiver.load('empty_kibana'));
|
||||
afterEach(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('create a note', () => {
|
||||
it('should return a timelineId, timelineVersion, noteId and version', async () => {
|
||||
const myNote = 'world test';
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelineNoteMutation,
|
||||
variables: {
|
||||
noteId: null,
|
||||
version: null,
|
||||
note: { note: myNote, timelineId: null },
|
||||
},
|
||||
});
|
||||
const { note, noteId, timelineId, timelineVersion, version } =
|
||||
response.data && response.data.persistNote.note;
|
||||
|
||||
expect(note).to.be(myNote);
|
||||
expect(noteId).to.not.be.empty();
|
||||
expect(timelineId).to.not.be.empty();
|
||||
expect(timelineVersion).to.not.be.empty();
|
||||
expect(version).to.not.be.empty();
|
||||
});
|
||||
|
||||
it('if noteId exist update note and return existing noteId and new version', async () => {
|
||||
const myNote = 'world test';
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelineNoteMutation,
|
||||
variables: {
|
||||
noteId: null,
|
||||
version: null,
|
||||
note: { note: myNote, timelineId: null },
|
||||
},
|
||||
});
|
||||
|
||||
const { noteId, timelineId, version } = response.data && response.data.persistNote.note;
|
||||
|
||||
const myNewNote = 'new world test';
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: persistTimelineNoteMutation,
|
||||
variables: {
|
||||
noteId,
|
||||
version,
|
||||
note: { note: myNewNote, timelineId },
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.persistNote.note.note).to.be(myNewNote);
|
||||
expect(responseToTest.data!.persistNote.note.noteId).to.be(noteId);
|
||||
expect(responseToTest.data!.persistNote.note.version).to.not.be.eql(version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete a note', () => {
|
||||
it('one note', async () => {
|
||||
const myNote = 'world test';
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelineNoteMutation,
|
||||
variables: {
|
||||
noteId: null,
|
||||
version: null,
|
||||
note: { note: myNote, timelineId: null },
|
||||
},
|
||||
});
|
||||
|
||||
const { noteId } = response.data && response.data.persistNote.note;
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: deleteNoteMutation,
|
||||
variables: {
|
||||
id: [noteId],
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.deleteNote).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default notesPersistenceTests;
|
||||
|
||||
const deleteNoteMutation = gql`
|
||||
mutation DeleteNoteMutation($id: [ID!]!) {
|
||||
deleteNote(id: $id)
|
||||
}
|
||||
`;
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { persistTimelinePinnedEventMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query';
|
||||
|
||||
import { KbnTestProvider } from '../types';
|
||||
|
||||
const pinnedEventsPersistenceTests: KbnTestProvider = ({ getService }) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('siemGraphQLClient');
|
||||
|
||||
describe('Pinned Events - Saved Objects', () => {
|
||||
beforeEach(() => esArchiver.load('empty_kibana'));
|
||||
afterEach(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('Pinned an event', () => {
|
||||
it('return a timelineId, timelineVersion, pinnedEventId and version', async () => {
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelinePinnedEventMutation,
|
||||
variables: {
|
||||
pinnedEventId: null,
|
||||
eventId: 'bv4QSGsB9v5HJNSH-7fi',
|
||||
},
|
||||
});
|
||||
const { eventId, pinnedEventId, timelineId, timelineVersion, version } =
|
||||
response.data && response.data.persistPinnedEventOnTimeline;
|
||||
|
||||
expect(eventId).to.be('bv4QSGsB9v5HJNSH-7fi');
|
||||
expect(pinnedEventId).to.not.be.empty();
|
||||
expect(timelineId).to.not.be.empty();
|
||||
expect(timelineVersion).to.not.be.empty();
|
||||
expect(version).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unpinned an event', () => {
|
||||
it('return null', async () => {
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelinePinnedEventMutation,
|
||||
variables: {
|
||||
pinnedEventId: null,
|
||||
eventId: 'bv4QSGsB9v5HJNSH-7fi',
|
||||
},
|
||||
});
|
||||
const { eventId, pinnedEventId } =
|
||||
response.data && response.data.persistPinnedEventOnTimeline;
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: persistTimelinePinnedEventMutation,
|
||||
variables: {
|
||||
pinnedEventId,
|
||||
eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.persistPinnedEventOnTimeline).to.be(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default pinnedEventsPersistenceTests;
|
320
x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts
Normal file
320
x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts
Normal file
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import ApolloClient from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
|
||||
import { deleteTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query';
|
||||
import { persistTimelineFavoriteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query';
|
||||
import { persistTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/persist.gql_query';
|
||||
import { TimelineResult } from '../../../../../legacy/plugins/siem/public/graphql/types';
|
||||
|
||||
import { KbnTestProvider } from '../types';
|
||||
|
||||
const timelinePersistenceTests: KbnTestProvider = ({ getService }) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('siemGraphQLClient');
|
||||
|
||||
describe('Timeline - Saved Objects', () => {
|
||||
beforeEach(() => esArchiver.load('empty_kibana'));
|
||||
afterEach(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('Persist a timeline', () => {
|
||||
it('Create a timeline just with a title', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId, title, version } =
|
||||
response.data && response.data.persistTimeline.timeline;
|
||||
|
||||
expect(title).to.be(titleToSaved);
|
||||
expect(savedObjectId).to.not.be.empty();
|
||||
expect(version).to.not.be.empty();
|
||||
});
|
||||
|
||||
it('Create a timeline with a full object', async () => {
|
||||
const timelineObject = {
|
||||
columns: [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: '@timestamp',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: '_index',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'message',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'event.category',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'event.action',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'host.name',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'source.ip',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'destination.ip',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
indexes: null,
|
||||
id: 'user.name',
|
||||
name: null,
|
||||
searchable: null,
|
||||
},
|
||||
],
|
||||
dataProviders: [
|
||||
{
|
||||
id: 'hosts-table-hostName-zeek-iowa',
|
||||
name: 'zeek-iowa',
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.name',
|
||||
displayField: null,
|
||||
value: 'zeek-iowa',
|
||||
displayValue: null,
|
||||
operator: ':',
|
||||
},
|
||||
and: [],
|
||||
},
|
||||
],
|
||||
description: 'some description',
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: 'kuery',
|
||||
expression: 'network.community_id : "1:pNuEtJ941SagdsAnFculFyREvXw=" ',
|
||||
},
|
||||
serializedQuery:
|
||||
'{"bool":{"should":[{"match_phrase":{"network.community_id":"1:pNuEtJ941SagdsAnFculFyREvXw="}}],"minimum_should_match":1}}',
|
||||
},
|
||||
},
|
||||
title: 'some title',
|
||||
dateRange: { start: 1560195800755, end: 1560282200756 },
|
||||
sort: { columnId: '@timestamp', sortDirection: 'desc' },
|
||||
};
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelineMutation,
|
||||
variables: {
|
||||
timelineId: null,
|
||||
version: null,
|
||||
timeline: timelineObject,
|
||||
},
|
||||
});
|
||||
const {
|
||||
columns,
|
||||
dataProviders,
|
||||
dateRange,
|
||||
description,
|
||||
kqlMode,
|
||||
kqlQuery,
|
||||
savedObjectId,
|
||||
sort,
|
||||
title,
|
||||
version,
|
||||
} = response.data && omitTypenameInTimeline(response.data.persistTimeline.timeline);
|
||||
|
||||
expect(columns.map((col: { id: string }) => col.id)).to.eql(
|
||||
timelineObject.columns.map(col => col.id)
|
||||
);
|
||||
expect(dataProviders).to.eql(timelineObject.dataProviders);
|
||||
expect(dateRange).to.eql(timelineObject.dateRange);
|
||||
expect(description).to.be(timelineObject.description);
|
||||
expect(kqlMode).to.be(timelineObject.kqlMode);
|
||||
expect(kqlQuery).to.eql(timelineObject.kqlQuery);
|
||||
expect(savedObjectId).to.not.be.empty();
|
||||
expect(sort).to.eql(timelineObject.sort);
|
||||
expect(title).to.be(timelineObject.title);
|
||||
expect(version).to.not.be.empty();
|
||||
});
|
||||
|
||||
it('Update a timeline with a new title', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId, version } = response.data && response.data.persistTimeline.timeline;
|
||||
|
||||
const newTitle = 'new title';
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: persistTimelineMutation,
|
||||
variables: {
|
||||
timelineId: savedObjectId,
|
||||
version,
|
||||
timeline: {
|
||||
title: newTitle,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.persistTimeline.timeline.savedObjectId).to.be(savedObjectId);
|
||||
expect(responseToTest.data!.persistTimeline.timeline.title).to.be(newTitle);
|
||||
expect(responseToTest.data!.persistTimeline.timeline.version).to.not.be.eql(version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Persist favorite', () => {
|
||||
it('to an existing timeline', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId, version } = response.data && response.data.persistTimeline.timeline;
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: persistTimelineFavoriteMutation,
|
||||
variables: {
|
||||
timelineId: savedObjectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
|
||||
expect(responseToTest.data!.persistFavorite.favorite.length).to.be(1);
|
||||
expect(responseToTest.data!.persistFavorite.version).to.not.be.eql(version);
|
||||
});
|
||||
|
||||
it('to Unfavorite an existing timeline', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId, version } = response.data && response.data.persistTimeline.timeline;
|
||||
|
||||
await client.mutate<any>({
|
||||
mutation: persistTimelineFavoriteMutation,
|
||||
variables: {
|
||||
timelineId: savedObjectId,
|
||||
},
|
||||
});
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: persistTimelineFavoriteMutation,
|
||||
variables: {
|
||||
timelineId: savedObjectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
|
||||
expect(responseToTest.data!.persistFavorite.favorite).to.be.empty();
|
||||
expect(responseToTest.data!.persistFavorite.version).to.not.be.eql(version);
|
||||
});
|
||||
|
||||
it('to a timeline without a timelineId', async () => {
|
||||
const response = await client.mutate<any>({
|
||||
mutation: persistTimelineFavoriteMutation,
|
||||
variables: {
|
||||
timelineId: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.data!.persistFavorite.savedObjectId).to.not.be.empty();
|
||||
expect(response.data!.persistFavorite.favorite.length).to.be(1);
|
||||
expect(response.data!.persistFavorite.version).to.not.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete', () => {
|
||||
it('one timeline', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response = await createBasicTimeline(client, titleToSaved);
|
||||
const { savedObjectId } = response.data && response.data.persistTimeline.timeline;
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: deleteTimelineMutation,
|
||||
variables: {
|
||||
id: [savedObjectId],
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.deleteTimeline).to.be(true);
|
||||
});
|
||||
|
||||
it('multiple timeline', async () => {
|
||||
const titleToSaved = 'hello title';
|
||||
const response1 = await createBasicTimeline(client, titleToSaved);
|
||||
const savedObjectId1 =
|
||||
response1.data && response1.data.persistTimeline.timeline
|
||||
? response1.data.persistTimeline.timeline.savedObjectId
|
||||
: '';
|
||||
|
||||
const response2 = await createBasicTimeline(client, titleToSaved);
|
||||
const savedObjectId2 =
|
||||
response2.data && response2.data.persistTimeline.timeline
|
||||
? response2.data.persistTimeline.timeline.savedObjectId
|
||||
: '';
|
||||
|
||||
const responseToTest = await client.mutate<any>({
|
||||
mutation: deleteTimelineMutation,
|
||||
variables: {
|
||||
id: [savedObjectId1, savedObjectId2],
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseToTest.data!.deleteTimeline).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default timelinePersistenceTests;
|
||||
|
||||
const omitTypename = (key: string, value: keyof TimelineResult) =>
|
||||
key === '__typename' ? undefined : value;
|
||||
|
||||
const omitTypenameInTimeline = (timeline: TimelineResult) =>
|
||||
JSON.parse(JSON.stringify(timeline), omitTypename);
|
||||
|
||||
const createBasicTimeline = async (client: ApolloClient<InMemoryCache>, titleToSaved: string) =>
|
||||
await client.mutate<any>({
|
||||
mutation: persistTimelineMutation,
|
||||
variables: {
|
||||
timelineId: null,
|
||||
version: null,
|
||||
timeline: {
|
||||
title: titleToSaved,
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue