mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* init routes for template timeline * create template timeline * add create/update timelines route * update api entry point * fix types * add template type * fix types * add types and template timeline id * fix types * update import timeline to handle template timeline * unit test * sudo code * remove class for savedobject * add template timeline version * clean up arguments * fix types for framework request * show filter in find * fix create template timeline * update mock data * handle missing timeline when exporting * update the order for timeline routes * update schemas * move type to common folder so we can re-use them on UI and server side * fix types + integrate persist with epic timeline * update all timeline when persit timeline * add timeline api readme * fix validation error * fix unit test * display error if unexpected format is given * fix issue with reftech all timeline query * fix flashing timeline while refetch * fix types * fix types * fix dependency * fix timeline deletion * remove redundant dependency * add i18n message * fix unit test Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7985bda96f
commit
f84e8b577f
56 changed files with 3054 additions and 1114 deletions
|
@ -7,14 +7,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import { SavedObjectsClient } from 'kibana/server';
|
||||
|
||||
import { unionWithNullType } from '../framework';
|
||||
import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types';
|
||||
import {
|
||||
PinnedEventToReturnSavedObjectRuntimeType,
|
||||
PinnedEventSavedObject,
|
||||
} from '../pinned_event/types';
|
||||
import { SavedObjectsClient } from '../../../../../../src/core/server';
|
||||
import { unionWithNullType } from '../../utility_types';
|
||||
import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note';
|
||||
import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event';
|
||||
|
||||
/*
|
||||
* ColumnHeader Types
|
||||
|
@ -136,6 +133,17 @@ const SavedSortRuntimeType = runtimeTypes.partial({
|
|||
/*
|
||||
* Timeline Types
|
||||
*/
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export const TimelineTypeLiteralRt = runtimeTypes.union([
|
||||
runtimeTypes.literal(TimelineType.template),
|
||||
runtimeTypes.literal(TimelineType.default),
|
||||
]);
|
||||
|
||||
export const SavedTimelineRuntimeType = runtimeTypes.partial({
|
||||
columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)),
|
||||
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
|
||||
|
@ -146,6 +154,9 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({
|
|||
kqlMode: unionWithNullType(runtimeTypes.string),
|
||||
kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType),
|
||||
title: unionWithNullType(runtimeTypes.string),
|
||||
templateTimelineId: unionWithNullType(runtimeTypes.string),
|
||||
templateTimelineVersion: unionWithNullType(runtimeTypes.number),
|
||||
timelineType: unionWithNullType(TimelineTypeLiteralRt),
|
||||
dateRange: unionWithNullType(SavedDateRangePickerRuntimeType),
|
||||
savedQueryId: unionWithNullType(runtimeTypes.string),
|
||||
sort: unionWithNullType(SavedSortRuntimeType),
|
||||
|
@ -192,6 +203,25 @@ export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection(
|
|||
export interface TimelineSavedObject
|
||||
extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {}
|
||||
|
||||
/**
|
||||
* All Timeline Saved object type with metadata
|
||||
*/
|
||||
export const TimelineResponseType = runtimeTypes.type({
|
||||
data: runtimeTypes.type({
|
||||
persistTimeline: runtimeTypes.intersection([
|
||||
runtimeTypes.partial({
|
||||
code: unionWithNullType(runtimeTypes.number),
|
||||
message: unionWithNullType(runtimeTypes.string),
|
||||
}),
|
||||
runtimeTypes.type({
|
||||
timeline: TimelineSavedToReturnObjectRuntimeType,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
export interface TimelineResponse extends runtimeTypes.TypeOf<typeof TimelineResponseType> {}
|
||||
|
||||
/**
|
||||
* All Timeline Saved object type with metadata
|
||||
*/
|
||||
|
@ -234,6 +264,11 @@ export type ExportedTimelines = TimelineSavedObject &
|
|||
pinnedEventIds: string[];
|
||||
};
|
||||
|
||||
export interface ExportTimelineNotFoundError {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BulkGetInput {
|
||||
type: string;
|
||||
id: string;
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
|
||||
import { unionWithNullType } from '../framework';
|
||||
import { unionWithNullType } from '../../../utility_types';
|
||||
|
||||
/*
|
||||
* Note Types
|
||||
|
@ -56,11 +56,7 @@ export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([
|
|||
version: runtimeTypes.string,
|
||||
}),
|
||||
runtimeTypes.partial({
|
||||
timelineVersion: runtimeTypes.union([
|
||||
runtimeTypes.string,
|
||||
runtimeTypes.null,
|
||||
runtimeTypes.undefined,
|
||||
]),
|
||||
timelineVersion: unionWithNullType(runtimeTypes.string),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
|
||||
import { unionWithNullType } from '../framework';
|
||||
import { unionWithNullType } from '../../../utility_types';
|
||||
|
||||
/*
|
||||
* Note Types
|
||||
|
@ -40,11 +40,7 @@ export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([
|
|||
}),
|
||||
runtimeTypes.partial({
|
||||
pinnedEventId: unionWithNullType(runtimeTypes.string),
|
||||
timelineVersion: runtimeTypes.union([
|
||||
runtimeTypes.string,
|
||||
runtimeTypes.null,
|
||||
runtimeTypes.undefined,
|
||||
]),
|
||||
timelineVersion: unionWithNullType(runtimeTypes.string),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -55,11 +51,7 @@ export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersecti
|
|||
}),
|
||||
SavedPinnedEventRuntimeType,
|
||||
runtimeTypes.partial({
|
||||
timelineVersion: runtimeTypes.union([
|
||||
runtimeTypes.string,
|
||||
runtimeTypes.null,
|
||||
runtimeTypes.undefined,
|
||||
]),
|
||||
timelineVersion: unionWithNullType(runtimeTypes.string),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// This type is for typing EuiDescriptionList
|
||||
|
@ -11,3 +12,6 @@ export interface DescriptionList {
|
|||
title: NonNullable<ReactNode>;
|
||||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
|
||||
runtimeTypes.union([type, runtimeTypes.null]);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`;
|
||||
exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKI gltyKM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`;
|
||||
|
||||
exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`;
|
||||
exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKI RNnzH\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { DeleteTimelines } from '../types';
|
|||
|
||||
import { TimelineDownloader } from './export_timeline';
|
||||
import { DeleteTimelineModalOverlay } from '../delete_timeline_modal';
|
||||
import { exportSelectedTimeline } from '../../../containers/timeline/all/api';
|
||||
import { exportSelectedTimeline } from '../../../containers/timeline/api';
|
||||
|
||||
export interface ExportTimeline {
|
||||
disableExportTimelineDownloader: () => void;
|
||||
|
|
|
@ -15,15 +15,34 @@ import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_pr
|
|||
import { mockOpenTimelineQueryResults } from '../../mock/timeline_results';
|
||||
import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page';
|
||||
|
||||
import { StatefulOpenTimeline } from '.';
|
||||
import { NotePreviews } from './note_previews';
|
||||
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
|
||||
|
||||
import { StatefulOpenTimeline } from '.';
|
||||
import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all';
|
||||
jest.mock('../../lib/kibana');
|
||||
jest.mock('../../containers/timeline/all', () => {
|
||||
const originalModule = jest.requireActual('../../containers/timeline/all');
|
||||
return {
|
||||
useGetAllTimeline: jest.fn(),
|
||||
getAllTimeline: originalModule.getAllTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
describe('StatefulOpenTimeline', () => {
|
||||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
const title = 'All Timelines / Open Timelines';
|
||||
beforeEach(() => {
|
||||
((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({
|
||||
fetchAllTimeline: jest.fn(),
|
||||
timelines: getAllTimeline(
|
||||
'',
|
||||
mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? []
|
||||
),
|
||||
loading: false,
|
||||
totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('it has the expected initial state', () => {
|
||||
const wrapper = mount(
|
||||
|
@ -459,6 +478,8 @@ describe('StatefulOpenTimeline', () => {
|
|||
.find('[data-test-subj="expand-notes"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true);
|
||||
expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -532,7 +553,7 @@ describe('StatefulOpenTimeline', () => {
|
|||
test('it renders the expected count of matching timelines when no query has been entered', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={theme}>
|
||||
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
|
||||
<MockedProvider addTypename={false}>
|
||||
<TestProviderWithoutDragAndDrop>
|
||||
<StatefulOpenTimeline
|
||||
data-test-subj="stateful-timeline"
|
||||
|
|
|
@ -11,8 +11,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||
import { Dispatch } from 'redux';
|
||||
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
|
||||
import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query';
|
||||
import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all';
|
||||
import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query';
|
||||
import { useGetAllTimeline } from '../../containers/timeline/all';
|
||||
import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types';
|
||||
import { State, timelineSelectors } from '../../store';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model';
|
||||
|
@ -104,6 +103,8 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
/** The requested field to sort on */
|
||||
const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD);
|
||||
|
||||
const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline();
|
||||
|
||||
/** Invoked when the user presses enters to submit the text in the search input */
|
||||
const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => {
|
||||
setSearch(query.queryText.trim());
|
||||
|
@ -133,45 +134,41 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
// }
|
||||
// };
|
||||
|
||||
const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback(
|
||||
(timelineIds: string[]) => {
|
||||
deleteTimelines(timelineIds, {
|
||||
search,
|
||||
pageInfo: {
|
||||
pageIndex: pageIndex + 1,
|
||||
pageSize,
|
||||
},
|
||||
sort: {
|
||||
sortField: sortField as SortFieldTimeline,
|
||||
sortOrder: sortDirection as Direction,
|
||||
},
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
const deleteTimelines: DeleteTimelines = useCallback(
|
||||
async (timelineIds: string[]) => {
|
||||
if (timelineIds.includes(timeline.savedObjectId || '')) {
|
||||
createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false });
|
||||
}
|
||||
await apolloClient.mutate<
|
||||
DeleteTimelineMutation.Mutation,
|
||||
DeleteTimelineMutation.Variables
|
||||
>({
|
||||
mutation: deleteTimelineMutation,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: { id: timelineIds },
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
[search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]
|
||||
[apolloClient, createNewTimeline, refetch, timeline]
|
||||
);
|
||||
|
||||
const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback(
|
||||
async (timelineIds: string[]) => {
|
||||
await deleteTimelines(timelineIds);
|
||||
},
|
||||
[deleteTimelines]
|
||||
);
|
||||
|
||||
/** Invoked when the user clicks the action to delete the selected timelines */
|
||||
const onDeleteSelected: OnDeleteSelected = useCallback(() => {
|
||||
deleteTimelines(getSelectedTimelineIds(selectedItems), {
|
||||
search,
|
||||
pageInfo: {
|
||||
pageIndex: pageIndex + 1,
|
||||
pageSize,
|
||||
},
|
||||
sort: {
|
||||
sortField: sortField as SortFieldTimeline,
|
||||
sortOrder: sortDirection as Direction,
|
||||
},
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
});
|
||||
const onDeleteSelected: OnDeleteSelected = useCallback(async () => {
|
||||
await deleteTimelines(getSelectedTimelineIds(selectedItems));
|
||||
|
||||
// NOTE: we clear the selection state below, but if the server fails to
|
||||
// delete a timeline, it will remain selected in the table:
|
||||
resetSelectionState();
|
||||
|
||||
// TODO: the query must re-execute to show the results of the deletion
|
||||
}, [selectedItems, search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]);
|
||||
}, [selectedItems, deleteTimelines]);
|
||||
|
||||
/** Invoked when the user selects (or de-selects) timelines */
|
||||
const onSelectionChange: OnSelectionChange = useCallback(
|
||||
|
@ -227,99 +224,88 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
[apolloClient, updateIsLoading, updateTimeline]
|
||||
);
|
||||
|
||||
const deleteTimelines: DeleteTimelines = useCallback(
|
||||
(timelineIds: string[], variables?: AllTimelinesVariables) => {
|
||||
if (timelineIds.includes(timeline.savedObjectId || '')) {
|
||||
createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false });
|
||||
}
|
||||
apolloClient.mutate<DeleteTimelineMutation.Mutation, DeleteTimelineMutation.Variables>({
|
||||
mutation: deleteTimelineMutation,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: { id: timelineIds },
|
||||
refetchQueries: [
|
||||
{
|
||||
query: allTimelinesQuery,
|
||||
variables,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
[apolloClient, createNewTimeline, timeline]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
focusInput();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AllTimelinesQuery
|
||||
pageInfo={{
|
||||
useEffect(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: pageIndex + 1,
|
||||
pageSize,
|
||||
}}
|
||||
search={search}
|
||||
sort={{ sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }}
|
||||
onlyUserFavorite={onlyFavorites}
|
||||
>
|
||||
{({ timelines, loading, totalCount, refetch }) => {
|
||||
return !isModal ? (
|
||||
<OpenTimeline
|
||||
data-test-subj={'open-timeline'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
importDataModalToggle={importDataModalToggle}
|
||||
onAddTimelinesToFavorites={undefined}
|
||||
onDeleteSelected={onDeleteSelected}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onOpenTimeline={openTimeline}
|
||||
onQueryChange={onQueryChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onTableChange={onTableChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
query={search}
|
||||
refetch={refetch}
|
||||
searchResults={timelines}
|
||||
setImportDataModalToggle={setImportDataModalToggle}
|
||||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
) : (
|
||||
<OpenTimelineModalBody
|
||||
data-test-subj={'open-timeline-modal'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
hideActions={hideActions}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
onAddTimelinesToFavorites={undefined}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onOpenTimeline={openTimeline}
|
||||
onQueryChange={onQueryChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onTableChange={onTableChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
query={search}
|
||||
searchResults={timelines}
|
||||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AllTimelinesQuery>
|
||||
},
|
||||
search,
|
||||
sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction },
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
timelines,
|
||||
totalCount,
|
||||
});
|
||||
}, [
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
sortField,
|
||||
sortDirection,
|
||||
timelines,
|
||||
totalCount,
|
||||
onlyFavorites,
|
||||
]);
|
||||
|
||||
return !isModal ? (
|
||||
<OpenTimeline
|
||||
data-test-subj={'open-timeline'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
importDataModalToggle={importDataModalToggle}
|
||||
onAddTimelinesToFavorites={undefined}
|
||||
onDeleteSelected={onDeleteSelected}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onOpenTimeline={openTimeline}
|
||||
onQueryChange={onQueryChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onTableChange={onTableChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
query={search}
|
||||
refetch={refetch}
|
||||
searchResults={timelines}
|
||||
setImportDataModalToggle={setImportDataModalToggle}
|
||||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
) : (
|
||||
<OpenTimelineModalBody
|
||||
data-test-subj={'open-timeline-modal'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
hideActions={hideActions}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
onAddTimelinesToFavorites={undefined}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onOpenTimeline={openTimeline}
|
||||
onQueryChange={onQueryChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onTableChange={onTableChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
query={search}
|
||||
searchResults={timelines}
|
||||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -328,7 +314,6 @@ const makeMapStateToProps = () => {
|
|||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const mapStateToProps = (state: State) => {
|
||||
const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults;
|
||||
|
||||
return {
|
||||
timeline,
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { TimelinesTable } from './timelines_table';
|
|||
import { TitleRow } from './title_row';
|
||||
import { ImportDataModal } from '../import_data_modal';
|
||||
import * as i18n from './translations';
|
||||
import { importTimelines } from '../../containers/timeline/all/api';
|
||||
import { importTimelines } from '../../containers/timeline/api';
|
||||
|
||||
import {
|
||||
UtilityBarGroup,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ThemeProvider } from 'styled-components';
|
|||
import { wait } from '../../../lib/helpers';
|
||||
import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers';
|
||||
import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results';
|
||||
import { useGetAllTimeline, getAllTimeline } from '../../../containers/timeline/all';
|
||||
|
||||
import { OpenTimelineModal } from '.';
|
||||
|
||||
|
@ -20,9 +21,28 @@ jest.mock('../../../lib/kibana');
|
|||
jest.mock('../../../utils/apollo_context', () => ({
|
||||
useApolloClient: () => ({}),
|
||||
}));
|
||||
jest.mock('../../../containers/timeline/all', () => {
|
||||
const originalModule = jest.requireActual('../../../containers/timeline/all');
|
||||
return {
|
||||
useGetAllTimeline: jest.fn(),
|
||||
getAllTimeline: originalModule.getAllTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
describe('OpenTimelineModal', () => {
|
||||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
beforeEach(() => {
|
||||
((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({
|
||||
fetchAllTimeline: jest.fn(),
|
||||
timelines: getAllTimeline(
|
||||
'',
|
||||
mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? []
|
||||
),
|
||||
loading: false,
|
||||
totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders the expected modal', async () => {
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AllTimelinesVariables } from '../../containers/timeline/all';
|
|||
import { TimelineModel } from '../../store/timeline/model';
|
||||
import { NoteResult } from '../../graphql/types';
|
||||
import { Refetch } from '../../store/inputs/model';
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
/** The users who added a timeline to favorites */
|
||||
export interface FavoriteTimelineResult {
|
||||
|
@ -47,6 +48,8 @@ export interface OpenTimelineResult {
|
|||
pinnedEventIds?: Readonly<Record<string, boolean>> | null;
|
||||
savedObjectId?: string | null;
|
||||
title?: string | null;
|
||||
templateTimelineId?: string | null;
|
||||
type?: TimelineType.template | TimelineType.default;
|
||||
updated?: number | null;
|
||||
updatedBy?: string | null;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import ApolloClient from 'apollo-client';
|
||||
import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { AllTimelinesQuery } from '../../containers/timeline/all';
|
||||
import { useGetAllTimeline } from '../../containers/timeline/all';
|
||||
import { SortFieldTimeline, Direction } from '../../graphql/types';
|
||||
import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers';
|
||||
import { OnOpenTimeline } from '../open_timeline/types';
|
||||
|
@ -62,35 +62,39 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
|
|||
[filterBy]
|
||||
);
|
||||
|
||||
return (
|
||||
<AllTimelinesQuery
|
||||
pageInfo={{
|
||||
const { fetchAllTimeline, timelines, totalCount, loading } = useGetAllTimeline();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
}}
|
||||
search={''}
|
||||
sort={{
|
||||
},
|
||||
search: '',
|
||||
sort: {
|
||||
sortField: SortFieldTimeline.updated,
|
||||
sortOrder: Direction.desc,
|
||||
}}
|
||||
onlyUserFavorite={filterBy === 'favorites'}
|
||||
>
|
||||
{({ timelines, loading }) => (
|
||||
<>
|
||||
{loading ? (
|
||||
loadingPlaceholders
|
||||
) : (
|
||||
<RecentTimelines
|
||||
noTimelinesMessage={noTimelinesMessage}
|
||||
onOpenTimeline={onOpenTimeline}
|
||||
timelines={timelines}
|
||||
/>
|
||||
)}
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiText size="xs">{linkAllTimelines}</EuiText>
|
||||
</>
|
||||
},
|
||||
onlyUserFavorite: filterBy === 'favorites',
|
||||
timelines,
|
||||
totalCount,
|
||||
});
|
||||
}, [filterBy, timelines, totalCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
loadingPlaceholders
|
||||
) : (
|
||||
<RecentTimelines
|
||||
noTimelinesMessage={noTimelinesMessage}
|
||||
onOpenTimeline={onOpenTimeline}
|
||||
timelines={timelines}
|
||||
/>
|
||||
)}
|
||||
</AllTimelinesQuery>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiText size="xs">{linkAllTimelines}</EuiText>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,11 +17,11 @@ import {
|
|||
EuiFilterButton,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { ListProps } from 'react-virtualized';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AllTimelinesQuery } from '../../../containers/timeline/all';
|
||||
import { useGetAllTimeline } from '../../../containers/timeline/all';
|
||||
import { SortFieldTimeline, Direction } from '../../../graphql/types';
|
||||
import { isUntitled } from '../../open_timeline/helpers';
|
||||
import * as i18nTimeline from '../../open_timeline/translations';
|
||||
|
@ -96,6 +96,7 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
|
|||
const [searchTimelineValue, setSearchTimelineValue] = useState('');
|
||||
const [onlyFavorites, setOnlyFavorites] = useState(false);
|
||||
const [searchRef, setSearchRef] = useState<HTMLElement | null>(null);
|
||||
const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline();
|
||||
|
||||
const onSearchTimeline = useCallback(val => {
|
||||
setSearchTimelineValue(val);
|
||||
|
@ -215,61 +216,64 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
|
|||
[searchRef, onlyFavorites, handleOnToggleOnlyFavorites]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: 1,
|
||||
pageSize,
|
||||
},
|
||||
search: searchTimelineValue,
|
||||
sort: {
|
||||
sortField: SortFieldTimeline.updated,
|
||||
sortOrder: Direction.desc,
|
||||
},
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
timelines,
|
||||
totalCount: timelineCount,
|
||||
});
|
||||
}, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AllTimelinesQuery
|
||||
pageInfo={{
|
||||
pageIndex: 1,
|
||||
pageSize,
|
||||
<EuiSelectableContainer isLoading={loading}>
|
||||
<EuiSelectable
|
||||
height={POPOVER_HEIGHT}
|
||||
isLoading={loading && timelines.length === 0}
|
||||
listProps={{
|
||||
rowHeight: TIMELINE_ITEM_HEIGHT,
|
||||
showIcons: false,
|
||||
virtualizedProps: ({
|
||||
onScroll: handleOnScroll.bind(
|
||||
null,
|
||||
timelines.filter(t => !hideUntitled || t.title !== '').length,
|
||||
timelineCount
|
||||
),
|
||||
} as unknown) as ListProps,
|
||||
}}
|
||||
search={searchTimelineValue}
|
||||
sort={{ sortField: SortFieldTimeline.updated, sortOrder: Direction.desc }}
|
||||
onlyUserFavorite={onlyFavorites}
|
||||
renderOption={renderTimelineOption}
|
||||
onChange={handleTimelineChange}
|
||||
searchable
|
||||
searchProps={{
|
||||
'data-test-subj': 'timeline-super-select-search-box',
|
||||
isLoading: loading,
|
||||
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
|
||||
onSearch: onSearchTimeline,
|
||||
incremental: false,
|
||||
inputRef: (ref: HTMLElement) => {
|
||||
setSearchRef(ref);
|
||||
},
|
||||
}}
|
||||
singleSelection={true}
|
||||
options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })}
|
||||
>
|
||||
{({ timelines, loading, totalCount }) => (
|
||||
<EuiSelectableContainer isLoading={loading}>
|
||||
<EuiSelectable
|
||||
height={POPOVER_HEIGHT}
|
||||
isLoading={loading && timelines.length === 0}
|
||||
listProps={{
|
||||
rowHeight: TIMELINE_ITEM_HEIGHT,
|
||||
showIcons: false,
|
||||
virtualizedProps: ({
|
||||
onScroll: handleOnScroll.bind(
|
||||
null,
|
||||
timelines.filter(t => !hideUntitled || t.title !== '').length,
|
||||
totalCount
|
||||
),
|
||||
} as unknown) as ListProps,
|
||||
}}
|
||||
renderOption={renderTimelineOption}
|
||||
onChange={handleTimelineChange}
|
||||
searchable
|
||||
searchProps={{
|
||||
'data-test-subj': 'timeline-super-select-search-box',
|
||||
isLoading: loading,
|
||||
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
|
||||
onSearch: onSearchTimeline,
|
||||
incremental: false,
|
||||
inputRef: (ref: HTMLElement) => {
|
||||
setSearchRef(ref);
|
||||
},
|
||||
}}
|
||||
singleSelection={true}
|
||||
options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
{favoritePortal}
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiSelectableContainer>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
{favoritePortal}
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</AllTimelinesQuery>
|
||||
</>
|
||||
</EuiSelectable>
|
||||
</EuiSelectableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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 { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../../common/constants';
|
||||
import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules';
|
||||
import { KibanaServices } from '../../../lib/kibana';
|
||||
import { ExportSelectedData } from '../../../components/generic_downloader';
|
||||
|
||||
export const importTimelines = async ({
|
||||
fileToImport,
|
||||
overwrite = false,
|
||||
signal,
|
||||
}: ImportDataProps): Promise<ImportDataResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToImport);
|
||||
|
||||
return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
query: { overwrite },
|
||||
body: formData,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSelectedTimeline: ExportSelectedData = async ({
|
||||
excludeExportDetails = false,
|
||||
filename = `timelines_export.ndjson`,
|
||||
ids = [],
|
||||
signal,
|
||||
}): Promise<Blob> => {
|
||||
const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined;
|
||||
const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
query: {
|
||||
exclude_export_details: excludeExportDetails,
|
||||
file_name: filename,
|
||||
},
|
||||
signal,
|
||||
asResponse: true,
|
||||
});
|
||||
|
||||
return response.body!;
|
||||
};
|
|
@ -55,6 +55,9 @@ export const allTimelinesQuery = gql`
|
|||
noteIds
|
||||
pinnedEventIds
|
||||
title
|
||||
timelineType
|
||||
templateTimelineId
|
||||
templateTimelineVersion
|
||||
created
|
||||
createdBy
|
||||
updated
|
||||
|
|
|
@ -3,23 +3,28 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { getOr } from 'lodash/fp';
|
||||
|
||||
import { getOr, noop } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Query } from 'react-apollo';
|
||||
|
||||
import { ApolloQueryResult } from 'apollo-client';
|
||||
import { OpenTimelineResult } from '../../../components/open_timeline/types';
|
||||
import { errorToToaster, useStateToaster } from '../../../components/toasters';
|
||||
import {
|
||||
GetAllTimeline,
|
||||
PageInfoTimeline,
|
||||
SortTimeline,
|
||||
TimelineResult,
|
||||
} from '../../../graphql/types';
|
||||
import { inputsModel, inputsActions } from '../../../store/inputs';
|
||||
import { useApolloClient } from '../../../utils/apollo_context';
|
||||
|
||||
import { allTimelinesQuery } from './index.gql_query';
|
||||
import * as i18n from '../../../pages/timelines/translations';
|
||||
|
||||
export interface AllTimelinesArgs {
|
||||
fetchAllTimeline: ({ onlyUserFavorite, pageInfo, search, sort }: AllTimelinesVariables) => void;
|
||||
timelines: OpenTimelineResult[];
|
||||
loading: boolean;
|
||||
totalCount: number;
|
||||
|
@ -31,17 +36,13 @@ export interface AllTimelinesVariables {
|
|||
pageInfo: PageInfoTimeline;
|
||||
search: string;
|
||||
sort: SortTimeline;
|
||||
timelines: OpenTimelineResult[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface OwnProps extends AllTimelinesVariables {
|
||||
children?: (args: AllTimelinesArgs) => React.ReactNode;
|
||||
}
|
||||
export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES';
|
||||
|
||||
type Refetch = (
|
||||
variables: GetAllTimeline.Variables | undefined
|
||||
) => Promise<ApolloQueryResult<GetAllTimeline.Query>>;
|
||||
|
||||
const getAllTimeline = memoizeOne(
|
||||
export const getAllTimeline = memoizeOne(
|
||||
(variables: string, timelines: TimelineResult[]): OpenTimelineResult[] =>
|
||||
timelines.map(timeline => ({
|
||||
created: timeline.created,
|
||||
|
@ -76,41 +77,117 @@ const getAllTimeline = memoizeOne(
|
|||
}))
|
||||
);
|
||||
|
||||
const AllTimelinesQueryComponent: React.FC<OwnProps> = ({
|
||||
children,
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
}) => {
|
||||
const variables: GetAllTimeline.Variables = {
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
};
|
||||
const handleRefetch = useCallback((refetch: Refetch) => refetch(variables), [variables]);
|
||||
export const useGetAllTimeline = (): AllTimelinesArgs => {
|
||||
const dispatch = useDispatch();
|
||||
const apolloClient = useApolloClient();
|
||||
const refetch = useRef<inputsModel.Refetch>();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const [allTimelines, setAllTimelines] = useState<AllTimelinesArgs>({
|
||||
fetchAllTimeline: noop,
|
||||
loading: false,
|
||||
refetch: refetch.current ?? noop,
|
||||
totalCount: 0,
|
||||
timelines: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Query<GetAllTimeline.Query, GetAllTimeline.Variables>
|
||||
query={allTimelinesQuery}
|
||||
fetchPolicy="network-only"
|
||||
notifyOnNetworkStatusChange
|
||||
variables={variables}
|
||||
>
|
||||
{({ data, loading, refetch }) =>
|
||||
children!({
|
||||
loading,
|
||||
refetch: handleRefetch.bind(null, refetch),
|
||||
totalCount: getOr(0, 'getAllTimeline.totalCount', data),
|
||||
timelines: getAllTimeline(
|
||||
JSON.stringify(variables),
|
||||
getOr([], 'getAllTimeline.timeline', data)
|
||||
),
|
||||
})
|
||||
}
|
||||
</Query>
|
||||
const fetchAllTimeline = useCallback(
|
||||
async ({
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
timelines,
|
||||
totalCount,
|
||||
}: AllTimelinesVariables) => {
|
||||
let didCancel = false;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (apolloClient != null) {
|
||||
setAllTimelines({
|
||||
...allTimelines,
|
||||
timelines: timelines ?? allTimelines.timelines,
|
||||
totalCount: totalCount ?? allTimelines.totalCount,
|
||||
loading: true,
|
||||
});
|
||||
const variables: GetAllTimeline.Variables = {
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
};
|
||||
const response = await apolloClient.query<
|
||||
GetAllTimeline.Query,
|
||||
GetAllTimeline.Variables
|
||||
>({
|
||||
query: allTimelinesQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
variables,
|
||||
context: {
|
||||
fetchOptions: {
|
||||
abortSignal: abortCtrl.signal,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!didCancel) {
|
||||
dispatch(
|
||||
inputsActions.setQuery({
|
||||
inputId: 'global',
|
||||
id: ALL_TIMELINE_QUERY_ID,
|
||||
loading: false,
|
||||
refetch: refetch.current ?? noop,
|
||||
inspect: null,
|
||||
})
|
||||
);
|
||||
setAllTimelines({
|
||||
fetchAllTimeline,
|
||||
loading: false,
|
||||
refetch: refetch.current ?? noop,
|
||||
totalCount: getOr(0, 'getAllTimeline.totalCount', response.data),
|
||||
timelines: getAllTimeline(
|
||||
JSON.stringify(variables),
|
||||
getOr([], 'getAllTimeline.timeline', response.data)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
errorToToaster({
|
||||
title: i18n.ERROR_FETCHING_TIMELINES_TITLE,
|
||||
error: error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
dispatchToaster,
|
||||
});
|
||||
setAllTimelines({
|
||||
fetchAllTimeline,
|
||||
loading: false,
|
||||
refetch: noop,
|
||||
totalCount: 0,
|
||||
timelines: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
refetch.current = fetchData;
|
||||
fetchData();
|
||||
return () => {
|
||||
didCancel = true;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
},
|
||||
[apolloClient, allTimelines]
|
||||
);
|
||||
};
|
||||
|
||||
export const AllTimelinesQuery = React.memo(AllTimelinesQueryComponent);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID }));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return {
|
||||
...allTimelines,
|
||||
fetchAllTimeline,
|
||||
refetch: refetch.current ?? noop,
|
||||
};
|
||||
};
|
||||
|
|
115
x-pack/plugins/siem/public/containers/timeline/api.ts
Normal file
115
x-pack/plugins/siem/public/containers/timeline/api.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { throwErrors } from '../../../../case/common/api';
|
||||
import {
|
||||
SavedTimeline,
|
||||
TimelineResponse,
|
||||
TimelineResponseType,
|
||||
} from '../../../common/types/timeline';
|
||||
import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants';
|
||||
|
||||
import { KibanaServices } from '../../lib/kibana';
|
||||
import { ExportSelectedData } from '../../components/generic_downloader';
|
||||
|
||||
import { createToasterPlainError } from '../case/utils';
|
||||
import { ImportDataProps, ImportDataResponse } from '../detection_engine/rules';
|
||||
|
||||
interface RequestPostTimeline {
|
||||
timeline: SavedTimeline;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface RequestPatchTimeline<T = string> extends RequestPostTimeline {
|
||||
timelineId: T;
|
||||
version: T;
|
||||
}
|
||||
|
||||
type RequestPersistTimeline = RequestPostTimeline & Partial<RequestPatchTimeline<null | string>>;
|
||||
|
||||
const decodeTimelineResponse = (respTimeline?: TimelineResponse) =>
|
||||
pipe(
|
||||
TimelineResponseType.decode(respTimeline),
|
||||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
const postTimeline = async ({ timeline }: RequestPostTimeline): Promise<TimelineResponse> => {
|
||||
const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ timeline }),
|
||||
});
|
||||
|
||||
return decodeTimelineResponse(response);
|
||||
};
|
||||
|
||||
const patchTimeline = async ({
|
||||
timelineId,
|
||||
timeline,
|
||||
version,
|
||||
}: RequestPatchTimeline): Promise<TimelineResponse> => {
|
||||
const response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ timeline, timelineId, version }),
|
||||
});
|
||||
|
||||
return decodeTimelineResponse(response);
|
||||
};
|
||||
|
||||
export const persistTimeline = async ({
|
||||
timelineId,
|
||||
timeline,
|
||||
version,
|
||||
}: RequestPersistTimeline): Promise<TimelineResponse> => {
|
||||
if (timelineId == null) {
|
||||
return postTimeline({ timeline });
|
||||
}
|
||||
return patchTimeline({
|
||||
timelineId,
|
||||
timeline,
|
||||
version: version ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
export const importTimelines = async ({
|
||||
fileToImport,
|
||||
overwrite = false,
|
||||
signal,
|
||||
}: ImportDataProps): Promise<ImportDataResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToImport);
|
||||
|
||||
return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
query: { overwrite },
|
||||
body: formData,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSelectedTimeline: ExportSelectedData = async ({
|
||||
excludeExportDetails = false,
|
||||
filename = `timelines_export.ndjson`,
|
||||
ids = [],
|
||||
signal,
|
||||
}): Promise<Blob> => {
|
||||
const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined;
|
||||
const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
query: {
|
||||
exclude_export_details: excludeExportDetails,
|
||||
file_name: filename,
|
||||
},
|
||||
signal,
|
||||
asResponse: true,
|
||||
});
|
||||
|
||||
return response.body!;
|
||||
};
|
|
@ -9728,6 +9728,30 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineId",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineVersion",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Int", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "timelineType",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "ENUM", "name": "TimelineType", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updated",
|
||||
"description": "",
|
||||
|
@ -10323,6 +10347,39 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "TimelineType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "template",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "PageInfoTimeline",
|
||||
|
@ -10863,6 +10920,24 @@
|
|||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineId",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineVersion",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "Int", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "timelineType",
|
||||
"description": "",
|
||||
"type": { "kind": "ENUM", "name": "TimelineType", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "dateRange",
|
||||
"description": "",
|
||||
|
|
|
@ -132,6 +132,12 @@ export interface TimelineInput {
|
|||
|
||||
title?: Maybe<string>;
|
||||
|
||||
templateTimelineId?: Maybe<string>;
|
||||
|
||||
templateTimelineVersion?: Maybe<number>;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
|
||||
dateRange?: Maybe<DateRangePickerInput>;
|
||||
|
||||
savedQueryId?: Maybe<string>;
|
||||
|
@ -334,6 +340,11 @@ export enum TlsFields {
|
|||
_id = '_id',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum SortFieldTimeline {
|
||||
title = 'title',
|
||||
description = 'description',
|
||||
|
@ -1944,6 +1955,12 @@ export interface TimelineResult {
|
|||
|
||||
title?: Maybe<string>;
|
||||
|
||||
templateTimelineId?: Maybe<string>;
|
||||
|
||||
templateTimelineVersion?: Maybe<number>;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
|
||||
updated?: Maybe<number>;
|
||||
|
||||
updatedBy?: Maybe<string>;
|
||||
|
@ -4030,6 +4047,12 @@ export namespace GetAllTimeline {
|
|||
|
||||
title: Maybe<string>;
|
||||
|
||||
timelineType: Maybe<TimelineType>;
|
||||
|
||||
templateTimelineId: Maybe<string>;
|
||||
|
||||
templateTimelineVersion: Maybe<number>;
|
||||
|
||||
created: Maybe<number>;
|
||||
|
||||
createdBy: Maybe<string>;
|
||||
|
|
|
@ -168,6 +168,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 1',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -294,6 +297,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 2',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -420,6 +426,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 2',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -546,6 +555,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 3',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -672,6 +684,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 4',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -798,6 +813,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 5',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -924,6 +942,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 6',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1050,6 +1071,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1176,6 +1200,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1302,6 +1329,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1428,6 +1458,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1554,6 +1587,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
@ -1680,6 +1716,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
createdBy: 'elastic',
|
||||
updated: 1558390951234,
|
||||
|
|
|
@ -23,3 +23,10 @@ export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate(
|
|||
defaultMessage: 'Import Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_FETCHING_TIMELINES_TITLE = i18n.translate(
|
||||
'xpack.siem.timelines.allTimelines.errorFetchingTimelinesTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to query all timelines data',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@ export const createStore = (
|
|||
|
||||
const middlewareDependencies = {
|
||||
apolloClient$: apolloClient,
|
||||
selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector,
|
||||
selectNotesByIdSelector: appSelectors.selectNotesByIdSelector,
|
||||
timelineByIdSelector: timelineSelectors.timelineByIdSelector,
|
||||
timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector,
|
||||
|
|
|
@ -29,17 +29,11 @@ import {
|
|||
} from 'rxjs/operators';
|
||||
|
||||
import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public';
|
||||
import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query';
|
||||
import {
|
||||
PersistTimelineMutation,
|
||||
TimelineInput,
|
||||
ResponseTimeline,
|
||||
TimelineResult,
|
||||
} from '../../graphql/types';
|
||||
import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types';
|
||||
import { AppApolloClient } from '../../lib/lib';
|
||||
import { addError } from '../app/actions';
|
||||
import { NotesById } from '../app/model';
|
||||
import { TimeRange } from '../inputs/model';
|
||||
import { inputsModel } from '../inputs';
|
||||
|
||||
import {
|
||||
applyKqlFilterQuery,
|
||||
|
@ -75,13 +69,15 @@ import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_p
|
|||
import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite';
|
||||
import { isNotNull } from './helpers';
|
||||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import { refetchQueries } from './refetch_queries';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import { ActionTimeline, TimelineById } from './types';
|
||||
import { persistTimeline } from '../../containers/timeline/api';
|
||||
import { ALL_TIMELINE_QUERY_ID } from '../../containers/timeline/all';
|
||||
|
||||
interface TimelineEpicDependencies<State> {
|
||||
timelineByIdSelector: (state: State) => TimelineById;
|
||||
timelineTimeRangeSelector: (state: State) => TimeRange;
|
||||
timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange;
|
||||
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
|
||||
selectNotesByIdSelector: (state: State) => NotesById;
|
||||
apolloClient$: Observable<AppApolloClient>;
|
||||
}
|
||||
|
@ -119,10 +115,24 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
> => (
|
||||
action$,
|
||||
state$,
|
||||
{ selectNotesByIdSelector, timelineByIdSelector, timelineTimeRangeSelector, apolloClient$ }
|
||||
{
|
||||
selectAllTimelineQuery,
|
||||
selectNotesByIdSelector,
|
||||
timelineByIdSelector,
|
||||
timelineTimeRangeSelector,
|
||||
apolloClient$,
|
||||
}
|
||||
) => {
|
||||
const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull));
|
||||
|
||||
const allTimelineQuery$ = state$.pipe(
|
||||
map(state => {
|
||||
const getQuery = selectAllTimelineQuery();
|
||||
return getQuery(state, ALL_TIMELINE_QUERY_ID);
|
||||
}),
|
||||
filter(isNotNull)
|
||||
);
|
||||
|
||||
const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull));
|
||||
|
||||
const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull));
|
||||
|
@ -168,33 +178,52 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
const version = myEpicTimelineId.getTimelineVersion();
|
||||
|
||||
if (timelineNoteActionsType.includes(action.type)) {
|
||||
return epicPersistNote(apolloClient, action, timeline, notes, action$, timeline$, notes$);
|
||||
return epicPersistNote(
|
||||
apolloClient,
|
||||
action,
|
||||
timeline,
|
||||
notes,
|
||||
action$,
|
||||
timeline$,
|
||||
notes$,
|
||||
allTimelineQuery$
|
||||
);
|
||||
} else if (timelinePinnedEventActionsType.includes(action.type)) {
|
||||
return epicPersistPinnedEvent(apolloClient, action, timeline, action$, timeline$);
|
||||
return epicPersistPinnedEvent(
|
||||
apolloClient,
|
||||
action,
|
||||
timeline,
|
||||
action$,
|
||||
timeline$,
|
||||
allTimelineQuery$
|
||||
);
|
||||
} else if (timelineFavoriteActionsType.includes(action.type)) {
|
||||
return epicPersistTimelineFavorite(apolloClient, action, timeline, action$, timeline$);
|
||||
return epicPersistTimelineFavorite(
|
||||
apolloClient,
|
||||
action,
|
||||
timeline,
|
||||
action$,
|
||||
timeline$,
|
||||
allTimelineQuery$
|
||||
);
|
||||
} else if (timelineActionsType.includes(action.type)) {
|
||||
return from(
|
||||
apolloClient.mutate<
|
||||
PersistTimelineMutation.Mutation,
|
||||
PersistTimelineMutation.Variables
|
||||
>({
|
||||
mutation: persistTimelineMutation,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: {
|
||||
timelineId,
|
||||
version,
|
||||
timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
},
|
||||
refetchQueries,
|
||||
persistTimeline({
|
||||
timelineId,
|
||||
version,
|
||||
timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$),
|
||||
mergeMap(([result, recentTimeline]) => {
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, allTimelineQuery]) => {
|
||||
const savedTimeline = recentTimeline[action.payload.id];
|
||||
const response: ResponseTimeline = get('data.persistTimeline', result);
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
response.code === 409
|
||||
? updateAutoSaveMsg({
|
||||
|
@ -261,7 +290,7 @@ const timelineInput: TimelineInput = {
|
|||
|
||||
export const convertTimelineAsInput = (
|
||||
timeline: TimelineModel,
|
||||
timelineTimeRange: TimeRange
|
||||
timelineTimeRange: inputsModel.TimeRange
|
||||
): TimelineInput =>
|
||||
Object.keys(timelineInput).reduce<TimelineInput>((acc, key) => {
|
||||
if (has(key, timeline)) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi
|
|||
import { refetchQueries } from './refetch_queries';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import { ActionTimeline, TimelineById } from './types';
|
||||
import { inputsModel } from '../inputs';
|
||||
|
||||
export const timelineFavoriteActionsType = [updateIsFavorite.type];
|
||||
|
||||
|
@ -34,7 +35,8 @@ export const epicPersistTimelineFavorite = (
|
|||
action: ActionTimeline,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>
|
||||
timeline$: Observable<TimelineById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
|
@ -50,12 +52,16 @@ export const epicPersistTimelineFavorite = (
|
|||
refetchQueries,
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$),
|
||||
mergeMap(([result, recentTimelines]) => {
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimelines, allTimelineQuery]) => {
|
||||
const savedTimeline = recentTimelines[action.payload.id];
|
||||
const response: ResponseFavoriteTimeline = get('data.persistFavorite', result);
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
...callOutMsg,
|
||||
updateTimeline({
|
||||
|
|
|
@ -16,6 +16,7 @@ import { persistTimelineNoteMutation } from '../../containers/timeline/notes/per
|
|||
import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types';
|
||||
import { updateNote, addError } from '../app/actions';
|
||||
import { NotesById } from '../app/model';
|
||||
import { inputsModel } from '../inputs';
|
||||
|
||||
import {
|
||||
addNote,
|
||||
|
@ -39,7 +40,8 @@ export const epicPersistNote = (
|
|||
notes: NotesById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>,
|
||||
notes$: Observable<NotesById>
|
||||
notes$: Observable<NotesById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
|
@ -61,12 +63,16 @@ export const epicPersistNote = (
|
|||
refetchQueries,
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, notes$),
|
||||
mergeMap(([result, recentTimeline, recentNotes]) => {
|
||||
withLatestFrom(timeline$, notes$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, recentNotes, allTimelineQuery]) => {
|
||||
const noteIdRedux = action.payload.noteId;
|
||||
const response: ResponseNote = get('data.persistNote', result);
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
...callOutMsg,
|
||||
recentTimeline[action.payload.id].savedObjectId == null
|
||||
|
|
|
@ -15,6 +15,8 @@ import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/ope
|
|||
import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query';
|
||||
import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types';
|
||||
import { addError } from '../app/actions';
|
||||
import { inputsModel } from '../inputs';
|
||||
|
||||
import {
|
||||
pinEvent,
|
||||
endTimelineSaving,
|
||||
|
@ -35,7 +37,8 @@ export const epicPersistPinnedEvent = (
|
|||
action: ActionTimeline,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>
|
||||
timeline$: Observable<TimelineById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
|
@ -57,12 +60,16 @@ export const epicPersistPinnedEvent = (
|
|||
refetchQueries,
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$),
|
||||
mergeMap(([result, recentTimeline]) => {
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, allTimelineQuery]) => {
|
||||
const savedTimeline = recentTimeline[action.payload.id];
|
||||
const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result);
|
||||
const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
response != null
|
||||
? updateTimeline({
|
||||
|
|
|
@ -125,6 +125,11 @@ export const timelineSchema = gql`
|
|||
script: String
|
||||
}
|
||||
|
||||
enum TimelineType {
|
||||
default
|
||||
template
|
||||
}
|
||||
|
||||
input TimelineInput {
|
||||
columns: [ColumnHeaderInput!]
|
||||
dataProviders: [DataProviderInput!]
|
||||
|
@ -134,6 +139,9 @@ export const timelineSchema = gql`
|
|||
kqlMode: String
|
||||
kqlQuery: SerializedFilterQueryInput
|
||||
title: String
|
||||
templateTimelineId: String
|
||||
templateTimelineVersion: Int
|
||||
timelineType: TimelineType
|
||||
dateRange: DateRangePickerInput
|
||||
savedQueryId: String
|
||||
sort: SortTimelineInput
|
||||
|
@ -237,6 +245,9 @@ export const timelineSchema = gql`
|
|||
savedObjectId: String!
|
||||
sort: SortTimelineResult
|
||||
title: String
|
||||
templateTimelineId: String
|
||||
templateTimelineVersion: Int
|
||||
timelineType: TimelineType
|
||||
updated: Float
|
||||
updatedBy: String
|
||||
version: String!
|
||||
|
|
|
@ -134,6 +134,12 @@ export interface TimelineInput {
|
|||
|
||||
title?: Maybe<string>;
|
||||
|
||||
templateTimelineId?: Maybe<string>;
|
||||
|
||||
templateTimelineVersion?: Maybe<number>;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
|
||||
dateRange?: Maybe<DateRangePickerInput>;
|
||||
|
||||
savedQueryId?: Maybe<string>;
|
||||
|
@ -336,6 +342,11 @@ export enum TlsFields {
|
|||
_id = '_id',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
default = 'default',
|
||||
template = 'template',
|
||||
}
|
||||
|
||||
export enum SortFieldTimeline {
|
||||
title = 'title',
|
||||
description = 'description',
|
||||
|
@ -1946,6 +1957,12 @@ export interface TimelineResult {
|
|||
|
||||
title?: Maybe<string>;
|
||||
|
||||
templateTimelineId?: Maybe<string>;
|
||||
|
||||
templateTimelineVersion?: Maybe<number>;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
|
||||
updated?: Maybe<number>;
|
||||
|
||||
updatedBy?: Maybe<string>;
|
||||
|
@ -8023,6 +8040,12 @@ export namespace TimelineResultResolvers {
|
|||
|
||||
title?: TitleResolver<Maybe<string>, TypeParent, TContext>;
|
||||
|
||||
templateTimelineId?: TemplateTimelineIdResolver<Maybe<string>, TypeParent, TContext>;
|
||||
|
||||
templateTimelineVersion?: TemplateTimelineVersionResolver<Maybe<number>, TypeParent, TContext>;
|
||||
|
||||
timelineType?: TimelineTypeResolver<Maybe<TimelineType>, TypeParent, TContext>;
|
||||
|
||||
updated?: UpdatedResolver<Maybe<number>, TypeParent, TContext>;
|
||||
|
||||
updatedBy?: UpdatedByResolver<Maybe<string>, TypeParent, TContext>;
|
||||
|
@ -8130,6 +8153,21 @@ export namespace TimelineResultResolvers {
|
|||
Parent = TimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type TemplateTimelineIdResolver<
|
||||
R = Maybe<string>,
|
||||
Parent = TimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type TemplateTimelineVersionResolver<
|
||||
R = Maybe<number>,
|
||||
Parent = TimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type TimelineTypeResolver<
|
||||
R = Maybe<TimelineType>,
|
||||
Parent = TimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type UpdatedResolver<
|
||||
R = Maybe<number>,
|
||||
Parent = TimelineResult,
|
||||
|
|
|
@ -27,9 +27,9 @@ import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status
|
|||
import { ConfigurationSourcesAdapter, Sources } from '../sources';
|
||||
import { AppBackendLibs, AppDomainLibs } from '../types';
|
||||
import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes';
|
||||
import { Note } from '../note/saved_object';
|
||||
import { PinnedEvent } from '../pinned_event/saved_object';
|
||||
import { Timeline } from '../timeline/saved_object';
|
||||
import * as note from '../note/saved_object';
|
||||
import * as pinnedEvent from '../pinned_event/saved_object';
|
||||
import * as timeline from '../timeline/saved_object';
|
||||
import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram';
|
||||
|
||||
export function compose(
|
||||
|
@ -41,10 +41,6 @@ export function compose(
|
|||
const sources = new Sources(new ConfigurationSourcesAdapter());
|
||||
const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework));
|
||||
|
||||
const timeline = new Timeline();
|
||||
const note = new Note();
|
||||
const pinnedEvent = new PinnedEvent();
|
||||
|
||||
const domainLibs: AppDomainLibs = {
|
||||
authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)),
|
||||
events: new Events(new ElasticsearchEventsAdapter(framework)),
|
||||
|
|
|
@ -15,6 +15,11 @@ import { identity } from 'fp-ts/lib/function';
|
|||
import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
|
||||
import { AuthenticatedUser } from '../../../../security/common/model';
|
||||
import { UNAUTHENTICATED_USER } from '../../../common/constants';
|
||||
import {
|
||||
SavedNote,
|
||||
NoteSavedObjectRuntimeType,
|
||||
NoteSavedObject,
|
||||
} from '../../../common/types/timeline/note';
|
||||
import {
|
||||
PageInfoNote,
|
||||
ResponseNote,
|
||||
|
@ -23,178 +28,198 @@ import {
|
|||
NoteResult,
|
||||
} from '../../graphql/types';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types';
|
||||
import { noteSavedObjectType } from './saved_object_mappings';
|
||||
import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
|
||||
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
|
||||
import { timelineSavedObjectType } from '../timeline/saved_object_mappings';
|
||||
|
||||
export class Note {
|
||||
public async deleteNote(request: FrameworkRequest, noteIds: string[]) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
await Promise.all(
|
||||
noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId))
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteNoteByTimelineId(request: FrameworkRequest, timelineId: string) {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const notesToBeDeleted = await this.getAllSavedNote(request, options);
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
await Promise.all(
|
||||
notesToBeDeleted.notes.map(note =>
|
||||
savedObjectsClient.delete(noteSavedObjectType, note.noteId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async getNote(request: FrameworkRequest, noteId: string): Promise<NoteSavedObject> {
|
||||
return this.getSavedNote(request, noteId);
|
||||
}
|
||||
|
||||
public async getNotesByEventId(
|
||||
request: FrameworkRequest,
|
||||
eventId: string
|
||||
): Promise<NoteSavedObject[]> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: eventId,
|
||||
searchFields: ['eventId'],
|
||||
};
|
||||
const notesByEventId = await this.getAllSavedNote(request, options);
|
||||
return notesByEventId.notes;
|
||||
}
|
||||
|
||||
public async getNotesByTimelineId(
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
): Promise<NoteSavedObject[]> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const notesByTimelineId = await this.getAllSavedNote(request, options);
|
||||
return notesByTimelineId.notes;
|
||||
}
|
||||
|
||||
public async getAllNotes(
|
||||
export interface Note {
|
||||
deleteNote: (request: FrameworkRequest, noteIds: string[]) => Promise<void>;
|
||||
deleteNoteByTimelineId: (request: FrameworkRequest, noteIds: string) => Promise<void>;
|
||||
getNote: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject>;
|
||||
getNotesByEventId: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject[]>;
|
||||
getNotesByTimelineId: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject[]>;
|
||||
getAllNotes: (
|
||||
request: FrameworkRequest,
|
||||
pageInfo: PageInfoNote | null,
|
||||
search: string | null,
|
||||
sort: SortNote | null
|
||||
): Promise<ResponseNotes> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: ['note'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
return this.getAllSavedNote(request, options);
|
||||
}
|
||||
|
||||
public async persistNote(
|
||||
) => Promise<ResponseNotes>;
|
||||
persistNote: (
|
||||
request: FrameworkRequest,
|
||||
noteId: string | null,
|
||||
version: string | null,
|
||||
note: SavedNote
|
||||
): Promise<ResponseNote> {
|
||||
try {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
) => Promise<ResponseNote>;
|
||||
convertSavedObjectToSavedNote: (
|
||||
savedObject: unknown,
|
||||
timelineVersion?: string | undefined | null
|
||||
) => NoteSavedObject;
|
||||
}
|
||||
|
||||
if (noteId == null) {
|
||||
const timelineVersionSavedObject =
|
||||
note.timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request.user)
|
||||
)
|
||||
);
|
||||
note.timelineId = timelineResult.savedObjectId;
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
// Create new note
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
note: convertSavedObjectToSavedNote(
|
||||
await savedObjectsClient.create(
|
||||
noteSavedObjectType,
|
||||
pickSavedNote(noteId, note, request.user)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
),
|
||||
};
|
||||
}
|
||||
await Promise.all(noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId)));
|
||||
};
|
||||
|
||||
// Update new note
|
||||
export const deleteNoteByTimelineId = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const notesToBeDeleted = await getAllSavedNote(request, options);
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
const existingNote = await this.getSavedNote(request, noteId);
|
||||
await Promise.all(
|
||||
notesToBeDeleted.notes.map(note => savedObjectsClient.delete(noteSavedObjectType, note.noteId))
|
||||
);
|
||||
};
|
||||
|
||||
export const getNote = async (
|
||||
request: FrameworkRequest,
|
||||
noteId: string
|
||||
): Promise<NoteSavedObject> => {
|
||||
return getSavedNote(request, noteId);
|
||||
};
|
||||
|
||||
export const getNotesByEventId = async (
|
||||
request: FrameworkRequest,
|
||||
eventId: string
|
||||
): Promise<NoteSavedObject[]> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: eventId,
|
||||
searchFields: ['eventId'],
|
||||
};
|
||||
const notesByEventId = await getAllSavedNote(request, options);
|
||||
return notesByEventId.notes;
|
||||
};
|
||||
|
||||
export const getNotesByTimelineId = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
): Promise<NoteSavedObject[]> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const notesByTimelineId = await getAllSavedNote(request, options);
|
||||
return notesByTimelineId.notes;
|
||||
};
|
||||
|
||||
export const getAllNotes = async (
|
||||
request: FrameworkRequest,
|
||||
pageInfo: PageInfoNote | null,
|
||||
search: string | null,
|
||||
sort: SortNote | null
|
||||
): Promise<ResponseNotes> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: noteSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: ['note'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
return getAllSavedNote(request, options);
|
||||
};
|
||||
|
||||
export const persistNote = async (
|
||||
request: FrameworkRequest,
|
||||
noteId: string | null,
|
||||
version: string | null,
|
||||
note: SavedNote
|
||||
): Promise<ResponseNote> => {
|
||||
try {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
if (noteId == null) {
|
||||
const timelineVersionSavedObject =
|
||||
note.timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request.user)
|
||||
)
|
||||
);
|
||||
note.timelineId = timelineResult.savedObjectId;
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
|
||||
// Create new note
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
note: convertSavedObjectToSavedNote(
|
||||
await savedObjectsClient.update(
|
||||
await savedObjectsClient.create(
|
||||
noteSavedObjectType,
|
||||
noteId,
|
||||
pickSavedNote(noteId, note, request.user),
|
||||
{
|
||||
version: existingNote.version || undefined,
|
||||
}
|
||||
)
|
||||
pickSavedNote(noteId, note, request.user)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
const noteToReturn: NoteResult = {
|
||||
...note,
|
||||
noteId: uuid.v1(),
|
||||
version: '',
|
||||
timelineId: '',
|
||||
timelineVersion: '',
|
||||
};
|
||||
return {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
note: noteToReturn,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getSavedNote(request: FrameworkRequest, NoteId: string) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId);
|
||||
|
||||
return convertSavedObjectToSavedNote(savedObject);
|
||||
}
|
||||
|
||||
private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
// Update new note
|
||||
|
||||
const existingNote = await getSavedNote(request, noteId);
|
||||
return {
|
||||
totalCount: savedObjects.total,
|
||||
notes: savedObjects.saved_objects.map(savedObject =>
|
||||
convertSavedObjectToSavedNote(savedObject)
|
||||
code: 200,
|
||||
message: 'success',
|
||||
note: convertSavedObjectToSavedNote(
|
||||
await savedObjectsClient.update(
|
||||
noteSavedObjectType,
|
||||
noteId,
|
||||
pickSavedNote(noteId, note, request.user),
|
||||
{
|
||||
version: existingNote.version || undefined,
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
const noteToReturn: NoteResult = {
|
||||
...note,
|
||||
noteId: uuid.v1(),
|
||||
version: '',
|
||||
timelineId: '',
|
||||
timelineVersion: '',
|
||||
};
|
||||
return {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
note: noteToReturn,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSavedNote = async (request: FrameworkRequest, NoteId: string) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId);
|
||||
|
||||
return convertSavedObjectToSavedNote(savedObject);
|
||||
};
|
||||
|
||||
const getAllSavedNote = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
|
||||
return {
|
||||
totalCount: savedObjects.total,
|
||||
notes: savedObjects.saved_objects.map(savedObject =>
|
||||
convertSavedObjectToSavedNote(savedObject)
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const convertSavedObjectToSavedNote = (
|
||||
savedObject: unknown,
|
||||
|
|
|
@ -13,175 +13,225 @@ import { identity } from 'fp-ts/lib/function';
|
|||
import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
|
||||
import { AuthenticatedUser } from '../../../../security/common/model';
|
||||
import { UNAUTHENTICATED_USER } from '../../../common/constants';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
import {
|
||||
PinnedEventSavedObject,
|
||||
PinnedEventSavedObjectRuntimeType,
|
||||
SavedPinnedEvent,
|
||||
} from './types';
|
||||
} from '../../../common/types/timeline/pinned_event';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
|
||||
import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types';
|
||||
import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
|
||||
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
|
||||
import { pinnedEventSavedObjectType } from './saved_object_mappings';
|
||||
import { timelineSavedObjectType } from '../timeline/saved_object_mappings';
|
||||
|
||||
export class PinnedEvent {
|
||||
public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
export interface PinnedEvent {
|
||||
deletePinnedEventOnTimeline: (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventIds: string[]
|
||||
) => Promise<void>;
|
||||
|
||||
await Promise.all(
|
||||
pinnedEventIds.map(pinnedEventId =>
|
||||
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId)
|
||||
)
|
||||
);
|
||||
}
|
||||
deleteAllPinnedEventsOnTimeline: (request: FrameworkRequest, timelineId: string) => Promise<void>;
|
||||
|
||||
public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options);
|
||||
await Promise.all(
|
||||
pinnedEventToBeDeleted.map(pinnedEvent =>
|
||||
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async getPinnedEvent(
|
||||
getPinnedEvent: (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventId: string
|
||||
): Promise<PinnedEventSavedObject> {
|
||||
return this.getSavedPinnedEvent(request, pinnedEventId);
|
||||
}
|
||||
) => Promise<PinnedEventSavedObject>;
|
||||
|
||||
public async getAllPinnedEventsByTimelineId(
|
||||
getAllPinnedEventsByTimelineId: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
): Promise<PinnedEventSavedObject[]> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
return this.getAllSavedPinnedEvents(request, options);
|
||||
}
|
||||
) => Promise<PinnedEventSavedObject[]>;
|
||||
|
||||
public async getAllPinnedEvents(
|
||||
getAllPinnedEvents: (
|
||||
request: FrameworkRequest,
|
||||
pageInfo: PageInfoNote | null,
|
||||
search: string | null,
|
||||
sort: SortNote | null
|
||||
): Promise<PinnedEventSavedObject[]> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: ['timelineId', 'eventId'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
return this.getAllSavedPinnedEvents(request, options);
|
||||
}
|
||||
) => Promise<PinnedEventSavedObject[]>;
|
||||
|
||||
public async persistPinnedEventOnTimeline(
|
||||
persistPinnedEventOnTimeline: (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventId: string | null, // pinned event saved object id
|
||||
eventId: string,
|
||||
timelineId: string | null
|
||||
): Promise<PinnedEventResponse | null> {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
) => Promise<PinnedEventResponse | null>;
|
||||
|
||||
try {
|
||||
if (pinnedEventId == null) {
|
||||
const timelineVersionSavedObject =
|
||||
timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request.user || null)
|
||||
)
|
||||
);
|
||||
timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
convertSavedObjectToSavedPinnedEvent: (
|
||||
savedObject: unknown,
|
||||
timelineVersion?: string | undefined | null
|
||||
) => PinnedEventSavedObject;
|
||||
|
||||
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 savedObjectsClient.create(
|
||||
pinnedEventSavedObjectType,
|
||||
pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
);
|
||||
}
|
||||
return isPinnedAlreadyExisting[0];
|
||||
}
|
||||
throw new Error('You can NOT pinned event without a timelineID');
|
||||
}
|
||||
// Delete Pinned Event on Timeline
|
||||
await this.deletePinnedEventOnTimeline(request, [pinnedEventId]);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 404) {
|
||||
/*
|
||||
* Why we are doing that, because if it is not found for sure that it will be unpinned
|
||||
* There is no need to bring back this error since we can assume that it is unpinned
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
return pinnedEventId != null
|
||||
? {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
pinnedEventId: eventId,
|
||||
timelineId: '',
|
||||
timelineVersion: '',
|
||||
}
|
||||
: null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId);
|
||||
|
||||
return convertSavedObjectToSavedPinnedEvent(savedObject);
|
||||
}
|
||||
|
||||
private async getAllSavedPinnedEvents(
|
||||
request: FrameworkRequest,
|
||||
options: SavedObjectsFindOptions
|
||||
) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
|
||||
return savedObjects.saved_objects.map(savedObject =>
|
||||
convertSavedObjectToSavedPinnedEvent(savedObject)
|
||||
);
|
||||
}
|
||||
pickSavedPinnedEvent: (
|
||||
pinnedEventId: string | null,
|
||||
savedPinnedEvent: SavedPinnedEvent,
|
||||
userInfo: AuthenticatedUser | null
|
||||
) => // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any;
|
||||
}
|
||||
|
||||
export const deletePinnedEventOnTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventIds: string[]
|
||||
) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
await Promise.all(
|
||||
pinnedEventIds.map(pinnedEventId =>
|
||||
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteAllPinnedEventsOnTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
const pinnedEventToBeDeleted = await getAllSavedPinnedEvents(request, options);
|
||||
await Promise.all(
|
||||
pinnedEventToBeDeleted.map(pinnedEvent =>
|
||||
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const getPinnedEvent = async (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventId: string
|
||||
): Promise<PinnedEventSavedObject> => {
|
||||
return getSavedPinnedEvent(request, pinnedEventId);
|
||||
};
|
||||
|
||||
export const getAllPinnedEventsByTimelineId = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
): Promise<PinnedEventSavedObject[]> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
search: timelineId,
|
||||
searchFields: ['timelineId'],
|
||||
};
|
||||
return getAllSavedPinnedEvents(request, options);
|
||||
};
|
||||
|
||||
export const getAllPinnedEvents = async (
|
||||
request: FrameworkRequest,
|
||||
pageInfo: PageInfoNote | null,
|
||||
search: string | null,
|
||||
sort: SortNote | null
|
||||
): Promise<PinnedEventSavedObject[]> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: pinnedEventSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: ['timelineId', 'eventId'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
return getAllSavedPinnedEvents(request, options);
|
||||
};
|
||||
|
||||
export const persistPinnedEventOnTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
pinnedEventId: string | null, // pinned event saved object id
|
||||
eventId: string,
|
||||
timelineId: string | null
|
||||
): Promise<PinnedEventResponse | null> => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
try {
|
||||
if (pinnedEventId == null) {
|
||||
const timelineVersionSavedObject =
|
||||
timelineId == null
|
||||
? await (async () => {
|
||||
const timelineResult = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(null, {}, request.user || null)
|
||||
)
|
||||
);
|
||||
timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign
|
||||
return timelineResult.version;
|
||||
})()
|
||||
: null;
|
||||
|
||||
if (timelineId != null) {
|
||||
const allPinnedEventId = await 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 savedObjectsClient.create(
|
||||
pinnedEventSavedObjectType,
|
||||
pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null)
|
||||
),
|
||||
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
|
||||
);
|
||||
}
|
||||
return isPinnedAlreadyExisting[0];
|
||||
}
|
||||
throw new Error('You can NOT pinned event without a timelineID');
|
||||
}
|
||||
// Delete Pinned Event on Timeline
|
||||
await deletePinnedEventOnTimeline(request, [pinnedEventId]);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 404) {
|
||||
/*
|
||||
* Why we are doing that, because if it is not found for sure that it will be unpinned
|
||||
* There is no need to bring back this error since we can assume that it is unpinned
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
return pinnedEventId != null
|
||||
? {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
pinnedEventId: eventId,
|
||||
timelineId: '',
|
||||
timelineVersion: '',
|
||||
}
|
||||
: null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getSavedPinnedEvent = async (request: FrameworkRequest, pinnedEventId: string) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId);
|
||||
|
||||
return convertSavedObjectToSavedPinnedEvent(savedObject);
|
||||
};
|
||||
|
||||
const getAllSavedPinnedEvents = async (
|
||||
request: FrameworkRequest,
|
||||
options: SavedObjectsFindOptions
|
||||
) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
|
||||
return savedObjects.saved_objects.map(savedObject =>
|
||||
convertSavedObjectToSavedPinnedEvent(savedObject)
|
||||
);
|
||||
};
|
||||
|
||||
export const convertSavedObjectToSavedPinnedEvent = (
|
||||
savedObject: unknown,
|
||||
timelineVersion?: string | undefined | null
|
||||
|
|
|
@ -8,16 +8,21 @@ import { failure } from 'io-ts/lib/PathReporter';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { TimelineSavedObjectRuntimeType, TimelineSavedObject } from './types';
|
||||
import {
|
||||
TimelineSavedObjectRuntimeType,
|
||||
TimelineSavedObject,
|
||||
} from '../../../common/types/timeline';
|
||||
|
||||
export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => {
|
||||
const timeline = pipe(
|
||||
TimelineSavedObjectRuntimeType.decode(savedObject),
|
||||
map(savedTimeline => ({
|
||||
savedObjectId: savedTimeline.id,
|
||||
version: savedTimeline.version,
|
||||
...savedTimeline.attributes,
|
||||
})),
|
||||
map(savedTimeline => {
|
||||
return {
|
||||
savedObjectId: savedTimeline.id,
|
||||
version: savedTimeline.version,
|
||||
...savedTimeline.attributes,
|
||||
};
|
||||
}),
|
||||
fold(errors => {
|
||||
throw new Error(failure(errors).join('\n'));
|
||||
}, identity)
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
|
||||
import { ImportTimelineResponse } from './routes/utils/import_timelines';
|
||||
import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema';
|
||||
import { BadRequestError } from '../detection_engine/errors/bad_request_error';
|
||||
|
||||
type ErrorFactory = (message: string) => Error;
|
||||
|
||||
|
@ -38,8 +39,11 @@ export const decodeOrThrow = <A, O, I>(
|
|||
pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity));
|
||||
|
||||
export const validateTimelines = (): Transform =>
|
||||
createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj));
|
||||
|
||||
createMapStream((obj: ImportTimelineResponse) =>
|
||||
obj instanceof Error
|
||||
? new BadRequestError(obj.message)
|
||||
: decodeOrThrow(ImportTimelinesSchemaRt)(obj)
|
||||
);
|
||||
export const createTimelinesStreamFromNdJson = (ruleLimit: number) => {
|
||||
return [
|
||||
createSplitStream('\n'),
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { AuthenticatedUser } from '../../../../security/common/model';
|
||||
import { UNAUTHENTICATED_USER } from '../../../common/constants';
|
||||
import { SavedTimeline } from './types';
|
||||
import { SavedTimeline, TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
export const pickSavedTimeline = (
|
||||
timelineId: string | null,
|
||||
|
@ -24,5 +25,21 @@ export const pickSavedTimeline = (
|
|||
savedTimeline.updated = dateNow;
|
||||
savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER;
|
||||
}
|
||||
|
||||
if (savedTimeline.timelineType === TimelineType.template) {
|
||||
savedTimeline.timelineType = TimelineType.template;
|
||||
if (savedTimeline.templateTimelineId == null) {
|
||||
savedTimeline.templateTimelineId = uuid.v4();
|
||||
}
|
||||
|
||||
if (savedTimeline.templateTimelineVersion == null) {
|
||||
savedTimeline.templateTimelineVersion = 1;
|
||||
}
|
||||
} else {
|
||||
savedTimeline.timelineType = TimelineType.default;
|
||||
savedTimeline.templateTimelineId = null;
|
||||
savedTimeline.templateTimelineVersion = null;
|
||||
}
|
||||
|
||||
return savedTimeline;
|
||||
};
|
||||
|
|
326
x-pack/plugins/siem/server/lib/timeline/routes/README.md
Normal file
326
x-pack/plugins/siem/server/lib/timeline/routes/README.md
Normal file
|
@ -0,0 +1,326 @@
|
|||
**Timeline apis**
|
||||
|
||||
1. Create timeline api
|
||||
2. Update timeline api
|
||||
3. Create template timeline api
|
||||
4. Update template timeline api
|
||||
|
||||
|
||||
## Create timeline api
|
||||
#### POST /api/timeline
|
||||
##### Authorization
|
||||
Type: Basic Auth
|
||||
username: Your Kibana username
|
||||
password: Your Kibana password
|
||||
|
||||
|
||||
##### Request header
|
||||
```
|
||||
Content-Type: application/json
|
||||
kbn-version: 8.0.0
|
||||
```
|
||||
##### Request body
|
||||
```json
|
||||
{
|
||||
"timeline": {
|
||||
"columns": [
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name"
|
||||
}
|
||||
],
|
||||
"dataProviders": [],
|
||||
"description": "",
|
||||
"eventType": "all",
|
||||
"filters": [],
|
||||
"kqlMode": "filter",
|
||||
"kqlQuery": {
|
||||
"filterQuery": null
|
||||
},
|
||||
"title": "abd",
|
||||
"dateRange": {
|
||||
"start": 1587370079200,
|
||||
"end": 1587456479201
|
||||
},
|
||||
"savedQueryId": null,
|
||||
"sort": {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc"
|
||||
}
|
||||
},
|
||||
"timelineId":null, // Leave this as null
|
||||
"version":null // Leave this as null
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Update timeline api
|
||||
#### PATCH /api/timeline
|
||||
##### Authorization
|
||||
Type: Basic Auth
|
||||
username: Your Kibana username
|
||||
password: Your Kibana password
|
||||
|
||||
|
||||
##### Request header
|
||||
```
|
||||
Content-Type: application/json
|
||||
kbn-version: 8.0.0
|
||||
```
|
||||
##### Request body
|
||||
```json
|
||||
{
|
||||
"timeline": {
|
||||
"columns": [
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name"
|
||||
}
|
||||
],
|
||||
"dataProviders": [],
|
||||
"description": "",
|
||||
"eventType": "all",
|
||||
"filters": [],
|
||||
"kqlMode": "filter",
|
||||
"kqlQuery": {
|
||||
"filterQuery": null
|
||||
},
|
||||
"title": "abd",
|
||||
"dateRange": {
|
||||
"start": 1587370079200,
|
||||
"end": 1587456479201
|
||||
},
|
||||
"savedQueryId": null,
|
||||
"sort": {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc"
|
||||
},
|
||||
"created": 1587468588922,
|
||||
"createdBy": "casetester",
|
||||
"updated": 1587468588922,
|
||||
"updatedBy": "casetester",
|
||||
"timelineType": "default"
|
||||
},
|
||||
"timelineId":"68ea5330-83c3-11ea-bff9-ab01dd7cb6cc", // Have to match the existing timeline savedObject id
|
||||
"version":"WzYwLDFd" // Have to match the existing timeline version
|
||||
}
|
||||
```
|
||||
|
||||
## Create template timeline api
|
||||
#### POST /api/timeline
|
||||
##### Authorization
|
||||
Type: Basic Auth
|
||||
username: Your Kibana username
|
||||
password: Your Kibana password
|
||||
|
||||
|
||||
##### Request header
|
||||
```
|
||||
Content-Type: application/json
|
||||
kbn-version: 8.0.0
|
||||
```
|
||||
##### Request body
|
||||
```json
|
||||
{
|
||||
"timeline": {
|
||||
"columns": [
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name"
|
||||
}
|
||||
],
|
||||
"dataProviders": [
|
||||
|
||||
],
|
||||
"description": "",
|
||||
"eventType": "all",
|
||||
"filters": [
|
||||
|
||||
],
|
||||
"kqlMode": "filter",
|
||||
"kqlQuery": {
|
||||
"filterQuery": null
|
||||
},
|
||||
"title": "abd",
|
||||
"dateRange": {
|
||||
"start": 1587370079200,
|
||||
"end": 1587456479201
|
||||
},
|
||||
"savedQueryId": null,
|
||||
"sort": {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc"
|
||||
},
|
||||
"timelineType": "template" // This is the difference between create timeline
|
||||
},
|
||||
"timelineId":null, // Leave this as null
|
||||
"version":null // Leave this as null
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Update template timeline api
|
||||
#### PATCH /api/timeline
|
||||
##### Authorization
|
||||
Type: Basic Auth
|
||||
username: Your Kibana username
|
||||
password: Your Kibana password
|
||||
|
||||
|
||||
##### Request header
|
||||
```
|
||||
Content-Type: application/json
|
||||
kbn-version: 8.0.0
|
||||
```
|
||||
##### Request body
|
||||
```json
|
||||
{
|
||||
"timeline": {
|
||||
"columns": [
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip"
|
||||
},
|
||||
{
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name"
|
||||
}
|
||||
],
|
||||
"dataProviders": [],
|
||||
"description": "",
|
||||
"eventType": "all",
|
||||
"filters": [],
|
||||
"kqlMode": "filter",
|
||||
"kqlQuery": {
|
||||
"filterQuery": null
|
||||
},
|
||||
"title": "abd",
|
||||
"dateRange": {
|
||||
"start": 1587370079200,
|
||||
"end": 1587456479201
|
||||
},
|
||||
"savedQueryId": null,
|
||||
"sort": {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc"
|
||||
},
|
||||
"timelineType": "template",
|
||||
"created": 1587473119992,
|
||||
"createdBy": "casetester",
|
||||
"updated": 1587473119992,
|
||||
"updatedBy": "casetester",
|
||||
"templateTimelineId": "745d0316-6af7-43bf-afd6-9747119754fb", // Please provide the existing template timeline version
|
||||
"templateTimelineVersion": 2 // Please provide a template timeline version grater than existing one
|
||||
},
|
||||
"timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well
|
||||
"version":"Wzg2LDFd" // Please provide the existing timeline version
|
||||
}
|
||||
```
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { omit } from 'lodash/fp';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
export const mockDuplicateIdErrors = [];
|
||||
|
||||
|
@ -148,6 +149,13 @@ export const mockGetTimelineValue = {
|
|||
pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'],
|
||||
};
|
||||
|
||||
export const mockGetTemplateTimelineValue = {
|
||||
...mockGetTimelineValue,
|
||||
timelineType: TimelineType.template,
|
||||
templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
templateTimelineVersion: 1,
|
||||
};
|
||||
|
||||
export const mockParsedTimelineObject = omit(
|
||||
[
|
||||
'globalNotes',
|
||||
|
|
|
@ -3,10 +3,18 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants';
|
||||
import { requestMock } from '../../../detection_engine/routes/__mocks__';
|
||||
import * as rt from 'io-ts';
|
||||
import {
|
||||
TIMELINE_EXPORT_URL,
|
||||
TIMELINE_IMPORT_URL,
|
||||
TIMELINE_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import stream from 'stream';
|
||||
import { requestMock } from '../../../detection_engine/routes/__mocks__';
|
||||
import { SavedTimeline, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { updateTimelineSchema } from '../schemas/update_timelines_schema';
|
||||
import { createTimelineSchema } from '../schemas/create_timelines_schema';
|
||||
|
||||
const readable = new stream.Readable();
|
||||
export const getExportTimelinesRequest = () =>
|
||||
requestMock.create({
|
||||
|
@ -31,6 +39,96 @@ export const getImportTimelinesRequest = (filename?: string) =>
|
|||
},
|
||||
});
|
||||
|
||||
export const inputTimeline: SavedTimeline = {
|
||||
columns: [
|
||||
{ columnHeaderType: 'not-filtered', id: '@timestamp' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'message' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'event.category' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'event.action' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'host.name' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'source.ip' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'destination.ip' },
|
||||
{ columnHeaderType: 'not-filtered', id: 'user.name' },
|
||||
],
|
||||
dataProviders: [],
|
||||
description: '',
|
||||
eventType: 'all',
|
||||
filters: [],
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: { filterQuery: null },
|
||||
title: 't',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
dateRange: { start: 1585227005527, end: 1585313405527 },
|
||||
savedQueryId: null,
|
||||
sort: { columnId: '@timestamp', sortDirection: 'desc' },
|
||||
};
|
||||
|
||||
export const inputTemplateTimeline = {
|
||||
...inputTimeline,
|
||||
timelineType: TimelineType.template,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
};
|
||||
|
||||
export const createTimelineWithoutTimelineId = {
|
||||
templateTimelineId: null,
|
||||
timeline: inputTimeline,
|
||||
timelineId: null,
|
||||
version: null,
|
||||
timelineType: TimelineType.default,
|
||||
};
|
||||
|
||||
export const createTemplateTimelineWithoutTimelineId = {
|
||||
templateTimelineId: null,
|
||||
timeline: inputTemplateTimeline,
|
||||
timelineId: null,
|
||||
version: null,
|
||||
timelineType: TimelineType.template,
|
||||
};
|
||||
|
||||
export const createTimelineWithTimelineId = {
|
||||
...createTimelineWithoutTimelineId,
|
||||
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
};
|
||||
|
||||
export const createTemplateTimelineWithTimelineId = {
|
||||
...createTemplateTimelineWithoutTimelineId,
|
||||
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
templateTimelineId: 'existing template timeline id',
|
||||
};
|
||||
|
||||
export const updateTimelineWithTimelineId = {
|
||||
timeline: inputTimeline,
|
||||
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
version: 'WzEyMjUsMV0=',
|
||||
};
|
||||
|
||||
export const updateTemplateTimelineWithTimelineId = {
|
||||
timeline: {
|
||||
...inputTemplateTimeline,
|
||||
templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
templateTimelineVersion: 2,
|
||||
},
|
||||
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
version: 'WzEyMjUsMV0=',
|
||||
};
|
||||
|
||||
export const getCreateTimelinesRequest = (mockBody: rt.TypeOf<typeof createTimelineSchema>) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
path: TIMELINE_URL,
|
||||
body: mockBody,
|
||||
});
|
||||
|
||||
export const getUpdateTimelinesRequest = (mockBody: rt.TypeOf<typeof updateTimelineSchema>) =>
|
||||
requestMock.create({
|
||||
method: 'patch',
|
||||
path: TIMELINE_URL,
|
||||
body: mockBody,
|
||||
});
|
||||
|
||||
export const getImportTimelinesRequestEnableOverwrite = (filename?: string) =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server';
|
||||
|
||||
import {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
createMockConfig,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
|
||||
import {
|
||||
mockGetCurrentUser,
|
||||
mockGetTimelineValue,
|
||||
mockGetTemplateTimelineValue,
|
||||
} from './__mocks__/import_timelines';
|
||||
import {
|
||||
getCreateTimelinesRequest,
|
||||
inputTimeline,
|
||||
createTimelineWithoutTimelineId,
|
||||
createTimelineWithTimelineId,
|
||||
createTemplateTimelineWithoutTimelineId,
|
||||
createTemplateTimelineWithTimelineId,
|
||||
} from './__mocks__/request_responses';
|
||||
import {
|
||||
CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
|
||||
CREATE_TIMELINE_ERROR_MESSAGE,
|
||||
} from './utils/create_timelines';
|
||||
|
||||
describe('create timelines', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let securitySetup: SecurityPluginSetup;
|
||||
let { context } = requestContextMock.createTools();
|
||||
let mockGetTimeline: jest.Mock;
|
||||
let mockPersistTimeline: jest.Mock;
|
||||
let mockPersistPinnedEventOnTimeline: jest.Mock;
|
||||
let mockPersistNote: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
context = requestContextMock.createTools().context;
|
||||
|
||||
securitySetup = ({
|
||||
authc: {
|
||||
getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser),
|
||||
},
|
||||
authz: {},
|
||||
} as unknown) as SecurityPluginSetup;
|
||||
|
||||
mockGetTimeline = jest.fn();
|
||||
mockPersistTimeline = jest.fn();
|
||||
mockPersistPinnedEventOnTimeline = jest.fn();
|
||||
mockPersistNote = jest.fn();
|
||||
});
|
||||
|
||||
describe('Manipulate timeline', () => {
|
||||
describe('Create a new timeline', () => {
|
||||
beforeEach(async () => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: createTimelineWithTimelineId,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const createTimelinesRoute = jest.requireActual('./create_timelines_route')
|
||||
.createTimelinesRoute;
|
||||
createTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
|
||||
const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId);
|
||||
await server.inject(mockRequest, context);
|
||||
});
|
||||
|
||||
test('should Create a new timeline savedObject', async () => {
|
||||
expect(mockPersistTimeline).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should Create a new timeline savedObject without timelineId', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][1]).toBeNull();
|
||||
});
|
||||
|
||||
test('should Create a new timeline savedObject without timeline version', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][2]).toBeNull();
|
||||
});
|
||||
|
||||
test('should Create a new timeline savedObject witn given timeline', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline);
|
||||
});
|
||||
|
||||
test('should NOT Create new pinned events', async () => {
|
||||
expect(mockPersistPinnedEventOnTimeline).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should NOT Create notes', async () => {
|
||||
expect(mockPersistNote).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('returns 200 when create timeline successfully', async () => {
|
||||
const response = await server.inject(
|
||||
getCreateTimelinesRequest(createTimelineWithoutTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import a timeline already exist', () => {
|
||||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const createTimelinesRoute = jest.requireActual('./create_timelines_route')
|
||||
.createTimelinesRoute;
|
||||
createTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('returns error message', async () => {
|
||||
const response = await server.inject(
|
||||
getCreateTimelinesRequest(createTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
message: CREATE_TIMELINE_ERROR_MESSAGE,
|
||||
status_code: 405,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manipulate template timeline', () => {
|
||||
describe('Create a new template timeline', () => {
|
||||
beforeEach(async () => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: createTemplateTimelineWithTimelineId,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const createTimelinesRoute = jest.requireActual('./create_timelines_route')
|
||||
.createTimelinesRoute;
|
||||
createTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
|
||||
const mockRequest = getCreateTimelinesRequest(createTemplateTimelineWithoutTimelineId);
|
||||
await server.inject(mockRequest, context);
|
||||
});
|
||||
|
||||
test('should Create a new template timeline savedObject', async () => {
|
||||
expect(mockPersistTimeline).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should Create a new template timeline savedObject without timelineId', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][1]).toBeNull();
|
||||
});
|
||||
|
||||
test('should Create a new template timeline savedObject without template timeline version', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][2]).toBeNull();
|
||||
});
|
||||
|
||||
test('should Create a new template timeline savedObject witn given template timeline', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(
|
||||
createTemplateTimelineWithTimelineId.timeline
|
||||
);
|
||||
});
|
||||
|
||||
test('should NOT Create new pinned events', async () => {
|
||||
expect(mockPersistPinnedEventOnTimeline).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should NOT Create notes', async () => {
|
||||
expect(mockPersistNote).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('returns 200 when create timeline successfully', async () => {
|
||||
const response = await server.inject(
|
||||
getCreateTimelinesRequest(createTimelineWithoutTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import a template timeline already exist', () => {
|
||||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const createTimelinesRoute = jest.requireActual('./create_timelines_route')
|
||||
.createTimelinesRoute;
|
||||
createTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('returns error message', async () => {
|
||||
const response = await server.inject(
|
||||
getCreateTimelinesRequest(createTemplateTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
message: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
|
||||
status_code: 405,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../../../../../../src/core/server';
|
||||
|
||||
import { TIMELINE_URL } from '../../../../common/constants';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
import { ConfigType } from '../../..';
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
|
||||
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';
|
||||
|
||||
export const createTimelinesRoute = (
|
||||
router: IRouter,
|
||||
config: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: TIMELINE_URL,
|
||||
validate: {
|
||||
body: buildRouteValidation(createTimelineSchema),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
const frameworkRequest = await buildFrameworkRequest(context, security, request);
|
||||
|
||||
const { timelineId, timeline, version } = request.body;
|
||||
const { templateTimelineId, 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;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// Create timeline
|
||||
const newTimeline = await createTimelines(frameworkRequest, timeline, null, version);
|
||||
return response.ok({
|
||||
body: {
|
||||
data: {
|
||||
persistTimeline: newTimeline,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
import { getImportTimelinesRequest } from './__mocks__/request_responses';
|
||||
import {
|
||||
createMockConfig,
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
requestMock,
|
||||
createMockConfig,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
import { TIMELINE_EXPORT_URL } from '../../../../common/constants';
|
||||
import { SecurityPluginSetup } from '../../../../../security/server';
|
||||
import { SecurityPluginSetup } from '../../../../../../plugins/security/server';
|
||||
|
||||
import {
|
||||
mockUniqueParsedObjects,
|
||||
|
@ -24,7 +24,6 @@ import {
|
|||
} from './__mocks__/import_timelines';
|
||||
|
||||
describe('import timelines', () => {
|
||||
let config: ReturnType<typeof createMockConfig>;
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let request: ReturnType<typeof requestMock.create>;
|
||||
let securitySetup: SecurityPluginSetup;
|
||||
|
@ -43,7 +42,6 @@ describe('import timelines', () => {
|
|||
|
||||
server = serverMock.create();
|
||||
context = requestContextMock.createTools().context;
|
||||
config = createMockConfig();
|
||||
|
||||
securitySetup = ({
|
||||
authc: {
|
||||
|
@ -84,40 +82,28 @@ describe('import timelines', () => {
|
|||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
Timeline: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion },
|
||||
}),
|
||||
};
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
PinnedEvent: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
}),
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
Note: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
}),
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const importTimelinesRoute = jest.requireActual('./import_timelines_route')
|
||||
.importTimelinesRoute;
|
||||
importTimelinesRoute(server.router, config, securitySetup);
|
||||
importTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('should use given timelineId to check if the timeline savedObject already exist', async () => {
|
||||
|
@ -230,38 +216,26 @@ describe('import timelines', () => {
|
|||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
Timeline: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
}),
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
PinnedEvent: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
}),
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
Note: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
}),
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const importTimelinesRoute = jest.requireActual('./import_timelines_route')
|
||||
.importTimelinesRoute;
|
||||
importTimelinesRoute(server.router, config, securitySetup);
|
||||
importTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('returns error message', async () => {
|
||||
|
@ -286,36 +260,24 @@ describe('import timelines', () => {
|
|||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
Timeline: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' },
|
||||
}),
|
||||
};
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
PinnedEvent: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue(
|
||||
new Error('Test error')
|
||||
),
|
||||
};
|
||||
}),
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue(
|
||||
new Error('Test error')
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
Note: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
}),
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -328,11 +290,14 @@ describe('import timelines', () => {
|
|||
const importTimelinesRoute = jest.requireActual('./import_timelines_route')
|
||||
.importTimelinesRoute;
|
||||
|
||||
importTimelinesRoute(server.router, config, securitySetup);
|
||||
importTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
const result = server.validate(request);
|
||||
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
'Invalid value "undefined" supplied to "file",Invalid value "undefined" supplied to "file"'
|
||||
[
|
||||
'Invalid value "undefined" supplied to "file"',
|
||||
'Invalid value "undefined" supplied to "file"',
|
||||
].join(',')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,19 @@
|
|||
*/
|
||||
|
||||
import { extname } from 'path';
|
||||
import { chunk, omit, set } from 'lodash/fp';
|
||||
import { chunk, omit } from 'lodash/fp';
|
||||
|
||||
import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils';
|
||||
import { IRouter } from '../../../../../../../src/core/server';
|
||||
|
||||
import { TIMELINE_IMPORT_URL } from '../../../../common/constants';
|
||||
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { ConfigType } from '../../../config';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
|
||||
import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema';
|
||||
import { validate } from '../../detection_engine/routes/rules/validate';
|
||||
import {
|
||||
buildSiemResponse,
|
||||
createBulkErrorObject,
|
||||
|
@ -16,32 +26,22 @@ import {
|
|||
} from '../../detection_engine/routes/utils';
|
||||
|
||||
import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson';
|
||||
import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils';
|
||||
|
||||
import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema';
|
||||
import { buildFrameworkRequest } from './utils/common';
|
||||
import {
|
||||
createTimelines,
|
||||
getTupleDuplicateErrorsAndUniqueTimeline,
|
||||
isBulkError,
|
||||
isImportRegular,
|
||||
ImportTimelineResponse,
|
||||
ImportTimelinesSchema,
|
||||
PromiseFromStreams,
|
||||
timelineSavedObjectOmittedFields,
|
||||
} from './utils/import_timelines';
|
||||
import { createTimelines, getTimeline } from './utils/create_timelines';
|
||||
|
||||
import { IRouter } from '../../../../../../../src/core/server';
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema';
|
||||
import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema';
|
||||
import { ConfigType } from '../../../config';
|
||||
|
||||
import { Timeline } from '../saved_object';
|
||||
import { validate } from '../../detection_engine/routes/rules/validate';
|
||||
import { FrameworkRequest } from '../../framework';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 10;
|
||||
|
||||
const timelineLib = new Timeline();
|
||||
|
||||
export const importTimelinesRoute = (
|
||||
router: IRouter,
|
||||
config: ConfigType,
|
||||
|
@ -95,9 +95,7 @@ export const importTimelinesRoute = (
|
|||
const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects);
|
||||
let importTimelineResponse: ImportTimelineResponse[] = [];
|
||||
|
||||
const user = await security?.authc.getCurrentUser(request);
|
||||
let frameworkRequest = set('context.core.savedObjects.client', savedObjectsClient, request);
|
||||
frameworkRequest = set('user', user, frameworkRequest);
|
||||
const frameworkRequest = await buildFrameworkRequest(context, security, request);
|
||||
|
||||
while (chunkParseObjects.length) {
|
||||
const batchParseObjects = chunkParseObjects.shift() ?? [];
|
||||
|
@ -125,32 +123,16 @@ export const importTimelinesRoute = (
|
|||
eventNotes,
|
||||
} = parsedTimeline;
|
||||
const parsedTimelineObject = omit(
|
||||
[
|
||||
'globalNotes',
|
||||
'eventNotes',
|
||||
'pinnedEventIds',
|
||||
'version',
|
||||
'savedObjectId',
|
||||
'created',
|
||||
'createdBy',
|
||||
'updated',
|
||||
'updatedBy',
|
||||
],
|
||||
timelineSavedObjectOmittedFields,
|
||||
parsedTimeline
|
||||
);
|
||||
let newTimeline = null;
|
||||
try {
|
||||
let timeline = null;
|
||||
try {
|
||||
timeline = await timelineLib.getTimeline(
|
||||
(frameworkRequest as unknown) as FrameworkRequest,
|
||||
savedObjectId
|
||||
);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
const timeline = await getTimeline(frameworkRequest, savedObjectId);
|
||||
|
||||
if (timeline == null) {
|
||||
const newSavedObjectId = await createTimelines(
|
||||
(frameworkRequest as unknown) as FrameworkRequest,
|
||||
newTimeline = await createTimelines(
|
||||
frameworkRequest,
|
||||
parsedTimelineObject,
|
||||
null, // timelineSavedObjectId
|
||||
null, // timelineVersion
|
||||
|
@ -159,7 +141,10 @@ export const importTimelinesRoute = (
|
|||
[] // existing note ids
|
||||
);
|
||||
|
||||
resolve({ timeline_id: newSavedObjectId, status_code: 200 });
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
});
|
||||
} else {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
|
||||
import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline';
|
||||
import { unionWithNullType } from '../../../../../common/utility_types';
|
||||
|
||||
export const createTimelineSchema = rt.intersection([
|
||||
rt.type({
|
||||
timeline: SavedTimelineRuntimeType,
|
||||
}),
|
||||
rt.partial({
|
||||
timelineId: unionWithNullType(rt.string),
|
||||
version: unionWithNullType(rt.string),
|
||||
}),
|
||||
]);
|
|
@ -7,14 +7,17 @@ import * as rt from 'io-ts';
|
|||
|
||||
import { Readable } from 'stream';
|
||||
import { either } from 'fp-ts/lib/Either';
|
||||
|
||||
import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline';
|
||||
|
||||
import { eventNotes, globalNotes, pinnedEventIds } from './schemas';
|
||||
import { SavedTimelineRuntimeType } from '../../types';
|
||||
import { unionWithNullType } from '../../../../../common/utility_types';
|
||||
|
||||
export const ImportTimelinesSchemaRt = rt.intersection([
|
||||
SavedTimelineRuntimeType,
|
||||
rt.type({
|
||||
savedObjectId: rt.string,
|
||||
version: rt.string,
|
||||
savedObjectId: unionWithNullType(rt.string),
|
||||
version: unionWithNullType(rt.string),
|
||||
}),
|
||||
rt.type({
|
||||
globalNotes,
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import { unionWithNullType } from '../../../framework';
|
||||
import { SavedNoteRuntimeType } from '../../../note/types';
|
||||
import { unionWithNullType } from '../../../../../common/utility_types';
|
||||
import { SavedNoteRuntimeType } from '../../../../../common/types/timeline/note';
|
||||
|
||||
export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType));
|
||||
export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType));
|
||||
export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string));
|
||||
export const eventNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType));
|
||||
export const globalNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType));
|
||||
export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string));
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
|
||||
import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline';
|
||||
import { unionWithNullType } from '../../../../../common/utility_types';
|
||||
|
||||
export const updateTimelineSchema = rt.type({
|
||||
timeline: SavedTimelineRuntimeType,
|
||||
timelineId: unionWithNullType(rt.string),
|
||||
version: unionWithNullType(rt.string),
|
||||
});
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server';
|
||||
|
||||
import {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
createMockConfig,
|
||||
} from '../../detection_engine/routes/__mocks__';
|
||||
|
||||
import {
|
||||
getUpdateTimelinesRequest,
|
||||
inputTimeline,
|
||||
updateTimelineWithTimelineId,
|
||||
updateTemplateTimelineWithTimelineId,
|
||||
} from './__mocks__/request_responses';
|
||||
import {
|
||||
mockGetCurrentUser,
|
||||
mockGetTimelineValue,
|
||||
mockGetTemplateTimelineValue,
|
||||
} from './__mocks__/import_timelines';
|
||||
import {
|
||||
UPDATE_TIMELINE_ERROR_MESSAGE,
|
||||
UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
|
||||
} from './utils/update_timelines';
|
||||
|
||||
describe('update timelines', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
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;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
context = requestContextMock.createTools().context;
|
||||
|
||||
securitySetup = ({
|
||||
authc: {
|
||||
getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser),
|
||||
},
|
||||
authz: {},
|
||||
} as unknown) as SecurityPluginSetup;
|
||||
|
||||
mockGetTimeline = jest.fn();
|
||||
mockGetTemplateTimeline = jest.fn();
|
||||
mockPersistTimeline = jest.fn();
|
||||
mockPersistPinnedEventOnTimeline = jest.fn();
|
||||
mockPersistNote = jest.fn();
|
||||
});
|
||||
|
||||
describe('Manipulate timeline', () => {
|
||||
describe('Update an existing timeline', () => {
|
||||
beforeEach(async () => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: updateTimelineWithTimelineId.timeline,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const updateTimelinesRoute = jest.requireActual('./update_timelines_route')
|
||||
.updateTimelinesRoute;
|
||||
updateTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
|
||||
const mockRequest = getUpdateTimelinesRequest(updateTimelineWithTimelineId);
|
||||
await server.inject(mockRequest, context);
|
||||
});
|
||||
|
||||
test('should Check a if given timeline id exist', async () => {
|
||||
expect(mockGetTimeline.mock.calls[0][1]).toEqual(updateTimelineWithTimelineId.timelineId);
|
||||
});
|
||||
|
||||
test('should Update existing timeline savedObject with timelineId', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][1]).toEqual(
|
||||
updateTimelineWithTimelineId.timelineId
|
||||
);
|
||||
});
|
||||
|
||||
test('should Update existing timeline savedObject with timeline version', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][2]).toEqual(updateTimelineWithTimelineId.version);
|
||||
});
|
||||
|
||||
test('should Update existing timeline savedObject witn given timeline', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline);
|
||||
});
|
||||
|
||||
test('should NOT Update new pinned events', async () => {
|
||||
expect(mockPersistPinnedEventOnTimeline).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should NOT Update notes', async () => {
|
||||
expect(mockPersistNote).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('returns 200 when create timeline successfully', async () => {
|
||||
const response = await server.inject(
|
||||
getUpdateTimelinesRequest(updateTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Update a timeline that doesn't exist", () => {
|
||||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const updateTimelinesRoute = jest.requireActual('./update_timelines_route')
|
||||
.updateTimelinesRoute;
|
||||
updateTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('returns error message', async () => {
|
||||
const response = await server.inject(
|
||||
getUpdateTimelinesRequest(updateTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
message: UPDATE_TIMELINE_ERROR_MESSAGE,
|
||||
status_code: 405,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manipulate template timeline', () => {
|
||||
describe('Update an existing template timeline', () => {
|
||||
beforeEach(async () => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
|
||||
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
|
||||
timeline: [mockGetTemplateTimelineValue],
|
||||
}),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: updateTimelineWithTimelineId.timeline,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const updateTimelinesRoute = jest.requireActual('./update_timelines_route')
|
||||
.updateTimelinesRoute;
|
||||
updateTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
|
||||
const mockRequest = getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId);
|
||||
await server.inject(mockRequest, context);
|
||||
});
|
||||
|
||||
test('should Check if given timeline id exist', async () => {
|
||||
expect(mockGetTimeline.mock.calls[0][1]).toEqual(
|
||||
updateTemplateTimelineWithTimelineId.timelineId
|
||||
);
|
||||
});
|
||||
|
||||
test('should Update existing template timeline with template timelineId', async () => {
|
||||
expect(mockGetTemplateTimeline.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
|
||||
);
|
||||
});
|
||||
|
||||
test('should Update existing template timeline witn given timeline', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][3]).toEqual(
|
||||
updateTemplateTimelineWithTimelineId.timeline
|
||||
);
|
||||
});
|
||||
|
||||
test('should NOT Update new pinned events', async () => {
|
||||
expect(mockPersistPinnedEventOnTimeline).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should NOT Update notes', async () => {
|
||||
expect(mockPersistNote).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('returns 200 when create template timeline successfully', async () => {
|
||||
const response = await server.inject(
|
||||
getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Update a template timeline that doesn't exist", () => {
|
||||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
|
||||
timeline: [],
|
||||
}),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../pinned_event/saved_object', () => {
|
||||
return {
|
||||
persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
|
||||
};
|
||||
});
|
||||
|
||||
jest.doMock('../../note/saved_object', () => {
|
||||
return {
|
||||
persistNote: mockPersistNote,
|
||||
};
|
||||
});
|
||||
|
||||
const updateTimelinesRoute = jest.requireActual('./update_timelines_route')
|
||||
.updateTimelinesRoute;
|
||||
updateTimelinesRoute(server.router, createMockConfig(), securitySetup);
|
||||
});
|
||||
|
||||
test('returns error message', async () => {
|
||||
const response = await server.inject(
|
||||
getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId),
|
||||
context
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
message: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
|
||||
status_code: 405,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../../../../../../src/core/server';
|
||||
|
||||
import { TIMELINE_URL } from '../../../../common/constants';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
|
||||
import { ConfigType } from '../../..';
|
||||
|
||||
import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils';
|
||||
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';
|
||||
|
||||
export const updateTimelinesRoute = (
|
||||
router: IRouter,
|
||||
config: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.patch(
|
||||
{
|
||||
path: TIMELINE_URL,
|
||||
validate: {
|
||||
body: buildRouteValidation(updateTimelineSchema),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line complexity
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { set } from 'lodash/fp';
|
||||
|
||||
import { SetupPlugins } from '../../../../plugin';
|
||||
import { KibanaRequest } from '../../../../../../../../src/core/server';
|
||||
import { RequestHandlerContext } from '../../../../../../../../target/types/core/server';
|
||||
import { FrameworkRequest } from '../../../framework';
|
||||
|
||||
export const buildFrameworkRequest = async (
|
||||
context: RequestHandlerContext,
|
||||
security: SetupPlugins['security'],
|
||||
request: KibanaRequest
|
||||
): Promise<FrameworkRequest> => {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
const user = await security?.authc.getCurrentUser(request);
|
||||
|
||||
return set<FrameworkRequest>(
|
||||
'user',
|
||||
user,
|
||||
set<KibanaRequest & { context: RequestHandlerContext }>(
|
||||
'context.core.savedObjects.client',
|
||||
savedObjectsClient,
|
||||
request
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash/fp';
|
||||
|
||||
import * as timelineLib from '../../saved_object';
|
||||
import * as pinnedEventLib from '../../../pinned_event/saved_object';
|
||||
import * as noteLib from '../../../note/saved_object';
|
||||
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 =
|
||||
'UPDATE template timeline with POST is not allowed, please use PATCH instead';
|
||||
|
||||
export const saveTimelines = (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timeline: SavedTimeline,
|
||||
timelineSavedObjectId?: string | null,
|
||||
timelineVersion?: string | null
|
||||
): Promise<ResponseTimeline> => {
|
||||
return timelineLib.persistTimeline(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? null,
|
||||
timelineVersion ?? null,
|
||||
timeline
|
||||
);
|
||||
};
|
||||
|
||||
export const savePinnedEvents = (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timelineSavedObjectId: string,
|
||||
pinnedEventIds: string[]
|
||||
) =>
|
||||
Promise.all(
|
||||
pinnedEventIds.map(eventId =>
|
||||
pinnedEventLib.persistPinnedEventOnTimeline(
|
||||
frameworkRequest,
|
||||
null, // pinnedEventSavedObjectId
|
||||
eventId,
|
||||
timelineSavedObjectId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const saveNotes = (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timelineSavedObjectId: string,
|
||||
timelineVersion?: string | null,
|
||||
existingNoteIds?: string[],
|
||||
newNotes?: NoteResult[]
|
||||
) => {
|
||||
return Promise.all(
|
||||
newNotes?.map(note => {
|
||||
const newNote: SavedNote = {
|
||||
eventId: note.eventId,
|
||||
note: note.note,
|
||||
timelineId: timelineSavedObjectId,
|
||||
};
|
||||
|
||||
return noteLib.persistNote(
|
||||
frameworkRequest,
|
||||
existingNoteIds?.find(nId => nId === note.noteId) ?? null,
|
||||
timelineVersion ?? null,
|
||||
newNote
|
||||
);
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const createTimelines = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timeline: SavedTimeline,
|
||||
timelineSavedObjectId?: string | null,
|
||||
timelineVersion?: string | null,
|
||||
pinnedEventIds?: string[] | null,
|
||||
notes?: NoteResult[],
|
||||
existingNoteIds?: string[]
|
||||
): Promise<ResponseTimeline> => {
|
||||
const responseTimeline = await saveTimelines(
|
||||
frameworkRequest,
|
||||
timeline,
|
||||
timelineSavedObjectId,
|
||||
timelineVersion
|
||||
);
|
||||
const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId;
|
||||
const newTimelineVersion = responseTimeline.timeline.version;
|
||||
|
||||
let myPromises: unknown[] = [];
|
||||
if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) {
|
||||
myPromises = [
|
||||
...myPromises,
|
||||
savePinnedEvents(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? newTimelineSavedObjectId,
|
||||
pinnedEventIds
|
||||
),
|
||||
];
|
||||
}
|
||||
if (!isEmpty(notes)) {
|
||||
myPromises = [
|
||||
...myPromises,
|
||||
saveNotes(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? newTimelineSavedObjectId,
|
||||
newTimelineVersion,
|
||||
existingNoteIds,
|
||||
notes
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (myPromises.length > 0) {
|
||||
await Promise.all(myPromises);
|
||||
}
|
||||
|
||||
return responseTimeline;
|
||||
};
|
||||
|
||||
export const getTimeline = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
savedObjectId: string
|
||||
): Promise<TimelineSavedObject | null> => {
|
||||
let timeline = null;
|
||||
try {
|
||||
timeline = await timelineLib.getTimeline(frameworkRequest, savedObjectId);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
return timeline;
|
||||
};
|
||||
|
||||
export const getTemplateTimeline = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
templateTimelineId: string
|
||||
): Promise<TimelineSavedObject | null> => {
|
||||
let templateTimeline = null;
|
||||
try {
|
||||
templateTimeline = await timelineLib.getTimelineByTemplateTimelineId(
|
||||
frameworkRequest,
|
||||
templateTimelineId
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return templateTimeline.timeline[0];
|
||||
};
|
|
@ -4,13 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { NoteSavedObject } from '../../../note/types';
|
||||
import { PinnedEventSavedObject } from '../../../pinned_event/types';
|
||||
import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline';
|
||||
|
||||
import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object';
|
||||
import { convertSavedObjectToSavedNote } from '../../../note/saved_object';
|
||||
|
||||
import {
|
||||
SavedObjectsClient,
|
||||
SavedObjectsFindOptions,
|
||||
|
@ -22,11 +15,20 @@ import {
|
|||
ExportTimelineSavedObjectsClient,
|
||||
ExportedNotes,
|
||||
TimelineSavedObject,
|
||||
} from '../../types';
|
||||
ExportTimelineNotFoundError,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { NoteSavedObject } from '../../../../../common/types/timeline/note';
|
||||
import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event';
|
||||
|
||||
import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson';
|
||||
|
||||
import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object';
|
||||
import { convertSavedObjectToSavedNote } from '../../../note/saved_object';
|
||||
import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings';
|
||||
import { noteSavedObjectType } from '../../../note/saved_object_mappings';
|
||||
|
||||
import { timelineSavedObjectType } from '../../saved_object_mappings';
|
||||
import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline';
|
||||
|
||||
export type TimelineSavedObjectsClient = Pick<
|
||||
SavedObjectsClient,
|
||||
|
@ -126,12 +128,23 @@ const getTimelines = async (
|
|||
)
|
||||
);
|
||||
|
||||
const timelineObjects: TimelineSavedObject[] | undefined =
|
||||
savedObjects != null
|
||||
? savedObjects.saved_objects.map((savedObject: unknown) => {
|
||||
return convertSavedObjectToSavedTimeline(savedObject);
|
||||
})
|
||||
: [];
|
||||
const timelineObjects: {
|
||||
timelines: TimelineSavedObject[];
|
||||
errors: ExportTimelineNotFoundError[];
|
||||
} = savedObjects.saved_objects.reduce(
|
||||
(acc, savedObject) => {
|
||||
return savedObject.error == null
|
||||
? {
|
||||
errors: acc.errors,
|
||||
timelines: [...acc.timelines, convertSavedObjectToSavedTimeline(savedObject)],
|
||||
}
|
||||
: { errors: [...acc.errors, savedObject.error], timelines: acc.timelines };
|
||||
},
|
||||
{
|
||||
timelines: [] as TimelineSavedObject[],
|
||||
errors: [] as ExportTimelineNotFoundError[],
|
||||
}
|
||||
);
|
||||
|
||||
return timelineObjects;
|
||||
};
|
||||
|
@ -139,12 +152,8 @@ const getTimelines = async (
|
|||
const getTimelinesFromObjects = async (
|
||||
savedObjectsClient: ExportTimelineSavedObjectsClient,
|
||||
ids: string[]
|
||||
): Promise<ExportedTimelines[]> => {
|
||||
const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, ids);
|
||||
// To Do for feature freeze
|
||||
// if (timelines.length !== request.body.ids.length) {
|
||||
// //figure out which is missing to tell user
|
||||
// }
|
||||
): Promise<Array<ExportedTimelines | ExportTimelineNotFoundError>> => {
|
||||
const { timelines, errors } = await getTimelines(savedObjectsClient, ids);
|
||||
|
||||
const [notes, pinnedEventIds] = await Promise.all([
|
||||
Promise.all(ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId))),
|
||||
|
@ -178,7 +187,7 @@ const getTimelinesFromObjects = async (
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
return myResponse ?? [];
|
||||
return [...myResponse, ...errors] ?? [];
|
||||
};
|
||||
|
||||
export const getExportTimelineByObjectIds = async ({
|
||||
|
|
|
@ -7,20 +7,10 @@
|
|||
import uuid from 'uuid';
|
||||
import { has } from 'lodash/fp';
|
||||
import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils';
|
||||
import { PinnedEvent } from '../../../pinned_event/saved_object';
|
||||
import { Note } from '../../../note/saved_object';
|
||||
|
||||
import { Timeline } from '../../saved_object';
|
||||
import { SavedTimeline } from '../../types';
|
||||
import { FrameworkRequest } from '../../../framework';
|
||||
import { SavedNote } from '../../../note/types';
|
||||
import { SavedTimeline } from '../../../../../common/types/timeline';
|
||||
import { NoteResult } from '../../../../graphql/types';
|
||||
import { HapiReadableStream } from '../../../detection_engine/rules/types';
|
||||
|
||||
const pinnedEventLib = new PinnedEvent();
|
||||
const timelineLib = new Timeline();
|
||||
const noteLib = new Note();
|
||||
|
||||
export interface ImportTimelinesSchema {
|
||||
success: boolean;
|
||||
success_count: number;
|
||||
|
@ -84,100 +74,6 @@ export const getTupleDuplicateErrorsAndUniqueTimeline = (
|
|||
return [Array.from(errors.values()), Array.from(timelinesAcc.values())];
|
||||
};
|
||||
|
||||
export const saveTimelines = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timeline: SavedTimeline,
|
||||
timelineSavedObjectId?: string | null,
|
||||
timelineVersion?: string | null
|
||||
) => {
|
||||
const newTimelineRes = await timelineLib.persistTimeline(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? null,
|
||||
timelineVersion ?? null,
|
||||
timeline
|
||||
);
|
||||
|
||||
return {
|
||||
newTimelineSavedObjectId: newTimelineRes?.timeline?.savedObjectId ?? null,
|
||||
newTimelineVersion: newTimelineRes?.timeline?.version ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const savePinnedEvents = (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timelineSavedObjectId: string,
|
||||
pinnedEventIds?: string[] | null
|
||||
) => {
|
||||
return (
|
||||
pinnedEventIds?.map(eventId => {
|
||||
return pinnedEventLib.persistPinnedEventOnTimeline(
|
||||
frameworkRequest,
|
||||
null, // pinnedEventSavedObjectId
|
||||
eventId,
|
||||
timelineSavedObjectId
|
||||
);
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const saveNotes = (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timelineSavedObjectId: string,
|
||||
timelineVersion?: string | null,
|
||||
existingNoteIds?: string[],
|
||||
newNotes?: NoteResult[]
|
||||
) => {
|
||||
return Promise.all(
|
||||
newNotes?.map(note => {
|
||||
const newNote: SavedNote = {
|
||||
eventId: note.eventId,
|
||||
note: note.note,
|
||||
timelineId: timelineSavedObjectId,
|
||||
};
|
||||
|
||||
return noteLib.persistNote(
|
||||
frameworkRequest,
|
||||
existingNoteIds?.find(nId => nId === note.noteId) ?? null,
|
||||
timelineVersion ?? null,
|
||||
newNote
|
||||
);
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const createTimelines = async (
|
||||
frameworkRequest: FrameworkRequest,
|
||||
timeline: SavedTimeline,
|
||||
timelineSavedObjectId?: string | null,
|
||||
timelineVersion?: string | null,
|
||||
pinnedEventIds?: string[] | null,
|
||||
notes?: NoteResult[],
|
||||
existingNoteIds?: string[]
|
||||
) => {
|
||||
const { newTimelineSavedObjectId, newTimelineVersion } = await saveTimelines(
|
||||
frameworkRequest,
|
||||
timeline,
|
||||
timelineSavedObjectId,
|
||||
timelineVersion
|
||||
);
|
||||
await Promise.all([
|
||||
savePinnedEvents(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? newTimelineSavedObjectId,
|
||||
pinnedEventIds
|
||||
),
|
||||
saveNotes(
|
||||
frameworkRequest,
|
||||
timelineSavedObjectId ?? newTimelineSavedObjectId,
|
||||
newTimelineVersion,
|
||||
existingNoteIds,
|
||||
notes
|
||||
),
|
||||
]);
|
||||
|
||||
return newTimelineSavedObjectId;
|
||||
};
|
||||
|
||||
export const isImportRegular = (
|
||||
importTimelineResponse: ImportTimelineResponse
|
||||
): importTimelineResponse is ImportRegular => {
|
||||
|
@ -189,3 +85,15 @@ export const isBulkError = (
|
|||
): importRuleResponse is BulkError => {
|
||||
return has('error', importRuleResponse);
|
||||
};
|
||||
|
||||
export const timelineSavedObjectOmittedFields = [
|
||||
'globalNotes',
|
||||
'eventNotes',
|
||||
'pinnedEventIds',
|
||||
'version',
|
||||
'savedObjectId',
|
||||
'created',
|
||||
'createdBy',
|
||||
'updated',
|
||||
'updatedBy',
|
||||
];
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { TimelineSavedObject } from '../../../../../common/types/timeline';
|
||||
|
||||
export const UPDATE_TIMELINE_ERROR_MESSAGE =
|
||||
'CREATE timeline with PATCH is not allowed, please use POST instead';
|
||||
export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
|
||||
'CREATE template timeline with PATCH is not allowed, please use POST instead';
|
||||
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 checkIsFailureCases = (
|
||||
isHandlingTemplateTimeline: boolean,
|
||||
version: string | null,
|
||||
templateTimelineVersion: number | null,
|
||||
existTimeline: TimelineSavedObject | null,
|
||||
existTemplateTimeline: TimelineSavedObject | null
|
||||
) => {
|
||||
if (!isHandlingTemplateTimeline && existTimeline == null) {
|
||||
return {
|
||||
body: UPDATE_TIMELINE_ERROR_MESSAGE,
|
||||
statusCode: 405,
|
||||
};
|
||||
} else if (isHandlingTemplateTimeline && existTemplateTimeline == null) {
|
||||
// Throw error to create template timeline in patch
|
||||
return {
|
||||
body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
|
||||
statusCode: 405,
|
||||
};
|
||||
} else if (
|
||||
isHandlingTemplateTimeline &&
|
||||
existTimeline != null &&
|
||||
existTemplateTimeline != null &&
|
||||
existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId
|
||||
) {
|
||||
// Throw error you can not have a no matching between your timeline and your template timeline during an update
|
||||
return {
|
||||
body: NO_MATCH_ID_ERROR_MESSAGE,
|
||||
statusCode: 409,
|
||||
};
|
||||
} else if (!isHandlingTemplateTimeline && existTimeline?.version !== version) {
|
||||
// throw error 409 conflict timeline
|
||||
return {
|
||||
body: NO_MATCH_VERSION_ERROR_MESSAGE,
|
||||
statusCode: 409,
|
||||
};
|
||||
} else if (
|
||||
isHandlingTemplateTimeline &&
|
||||
existTemplateTimeline != null &&
|
||||
existTemplateTimeline.templateTimelineVersion == null &&
|
||||
existTemplateTimeline.version !== version
|
||||
) {
|
||||
// throw error 409 conflict timeline
|
||||
return {
|
||||
body: NO_MATCH_VERSION_ERROR_MESSAGE,
|
||||
statusCode: 409,
|
||||
};
|
||||
} else if (
|
||||
isHandlingTemplateTimeline &&
|
||||
templateTimelineVersion != null &&
|
||||
existTemplateTimeline != null &&
|
||||
existTemplateTimeline.templateTimelineVersion != null &&
|
||||
existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion
|
||||
) {
|
||||
// Throw error you can not update a template timeline version with an old version
|
||||
return {
|
||||
body: OLDER_VERSION_ERROR_MESSAGE,
|
||||
statusCode: 409,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -8,260 +8,308 @@ import { getOr } from 'lodash/fp';
|
|||
|
||||
import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
|
||||
import { UNAUTHENTICATED_USER } from '../../../common/constants';
|
||||
import { NoteSavedObject } from '../../../common/types/timeline/note';
|
||||
import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event';
|
||||
import { SavedTimeline, TimelineSavedObject, TimelineType } from '../../../common/types/timeline';
|
||||
import {
|
||||
ResponseTimeline,
|
||||
PageInfoTimeline,
|
||||
SortTimeline,
|
||||
ResponseFavoriteTimeline,
|
||||
TimelineResult,
|
||||
Maybe,
|
||||
} from '../../graphql/types';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
import { Note } from '../note/saved_object';
|
||||
import { NoteSavedObject } from '../note/types';
|
||||
import { PinnedEventSavedObject } from '../pinned_event/types';
|
||||
import { PinnedEvent } from '../pinned_event/saved_object';
|
||||
import * as note from '../note/saved_object';
|
||||
import * as pinnedEvent from '../pinned_event/saved_object';
|
||||
import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline';
|
||||
import { pickSavedTimeline } from './pick_saved_timeline';
|
||||
import { timelineSavedObjectType } from './saved_object_mappings';
|
||||
import { SavedTimeline, TimelineSavedObject } from './types';
|
||||
|
||||
interface ResponseTimelines {
|
||||
timeline: TimelineSavedObject[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class Timeline {
|
||||
private readonly note = new Note();
|
||||
private readonly pinnedEvent = new PinnedEvent();
|
||||
export interface ResponseTemplateTimeline {
|
||||
code?: Maybe<number>;
|
||||
|
||||
public async getTimeline(
|
||||
request: FrameworkRequest,
|
||||
timelineId: string
|
||||
): Promise<TimelineSavedObject> {
|
||||
return this.getSavedTimeline(request, timelineId);
|
||||
}
|
||||
message?: Maybe<string>;
|
||||
|
||||
public async getAllTimeline(
|
||||
templateTimeline: TimelineResult;
|
||||
}
|
||||
|
||||
export interface Timeline {
|
||||
getTimeline: (request: FrameworkRequest, timelineId: string) => Promise<TimelineSavedObject>;
|
||||
|
||||
getAllTimeline: (
|
||||
request: FrameworkRequest,
|
||||
onlyUserFavorite: boolean | null,
|
||||
pageInfo: PageInfoTimeline | null,
|
||||
search: string | null,
|
||||
sort: SortTimeline | null
|
||||
): Promise<ResponseTimelines> {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: timelineSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: onlyUserFavorite
|
||||
? ['title', 'description', 'favorite.keySearch']
|
||||
: ['title', 'description'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
) => Promise<ResponseTimelines>;
|
||||
|
||||
return this.getAllSavedTimeline(request, options);
|
||||
}
|
||||
|
||||
public async persistFavorite(
|
||||
persistFavorite: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null
|
||||
): Promise<ResponseFavoriteTimeline> {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
const fullName = request.user?.full_name ?? '';
|
||||
try {
|
||||
let timeline: SavedTimeline = {};
|
||||
if (timelineId != null) {
|
||||
const {
|
||||
eventIdToNoteIds,
|
||||
notes,
|
||||
noteIds,
|
||||
pinnedEventIds,
|
||||
pinnedEventsSaveObject,
|
||||
savedObjectId,
|
||||
version,
|
||||
...savedTimeline
|
||||
} = await this.getBasicSavedTimeline(request, timelineId);
|
||||
timelineId = savedObjectId; // eslint-disable-line no-param-reassign
|
||||
timeline = savedTimeline;
|
||||
}
|
||||
) => Promise<ResponseFavoriteTimeline>;
|
||||
|
||||
const userFavoriteTimeline = {
|
||||
keySearch: userName != null ? convertStringToBase64(userName) : null,
|
||||
favoriteDate: new Date().valueOf(),
|
||||
fullName,
|
||||
userName,
|
||||
};
|
||||
if (timeline.favorite != null) {
|
||||
const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex(
|
||||
user => user.userName === userName
|
||||
);
|
||||
|
||||
timeline.favorite =
|
||||
alreadyExistsTimelineFavoriteByUser > -1
|
||||
? [
|
||||
...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser),
|
||||
...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1),
|
||||
]
|
||||
: [...timeline.favorite, userFavoriteTimeline];
|
||||
} else if (timeline.favorite == null) {
|
||||
timeline.favorite = [userFavoriteTimeline];
|
||||
}
|
||||
|
||||
const persistResponse = await this.persistTimeline(request, timelineId, null, timeline);
|
||||
return {
|
||||
savedObjectId: persistResponse.timeline.savedObjectId,
|
||||
version: persistResponse.timeline.version,
|
||||
favorite:
|
||||
persistResponse.timeline.favorite != null
|
||||
? persistResponse.timeline.favorite.filter(fav => fav.userName === userName)
|
||||
: [],
|
||||
};
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
return {
|
||||
savedObjectId: '',
|
||||
version: '',
|
||||
favorite: [],
|
||||
code: 403,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async persistTimeline(
|
||||
persistTimeline: (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null,
|
||||
version: string | null,
|
||||
timeline: SavedTimeline
|
||||
): Promise<ResponseTimeline> {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
try {
|
||||
if (timelineId == null) {
|
||||
// Create new timeline
|
||||
const newTimeline = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(timelineId, timeline, request.user)
|
||||
)
|
||||
);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
timeline: newTimeline,
|
||||
};
|
||||
}
|
||||
// Update Timeline
|
||||
await savedObjectsClient.update(
|
||||
timelineSavedObjectType,
|
||||
timelineId,
|
||||
pickSavedTimeline(timelineId, timeline, request.user),
|
||||
{
|
||||
version: version || undefined,
|
||||
}
|
||||
timeline: SavedTimeline,
|
||||
timelineType?: TimelineType | null
|
||||
) => 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
|
||||
): Promise<TimelineSavedObject> => {
|
||||
return getSavedTimeline(request, timelineId);
|
||||
};
|
||||
|
||||
export const getTimelineByTemplateTimelineId = async (
|
||||
request: FrameworkRequest,
|
||||
templateTimelineId: string
|
||||
): Promise<{
|
||||
totalCount: number;
|
||||
timeline: TimelineSavedObject[];
|
||||
}> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: timelineSavedObjectType,
|
||||
filter: `siem-ui-timeline.attributes.templateTimelineId: ${templateTimelineId}`,
|
||||
};
|
||||
return getAllSavedTimeline(request, options);
|
||||
};
|
||||
|
||||
export const getAllTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
onlyUserFavorite: boolean | null,
|
||||
pageInfo: PageInfoTimeline | null,
|
||||
search: string | null,
|
||||
sort: SortTimeline | null
|
||||
): Promise<ResponseTimelines> => {
|
||||
const options: SavedObjectsFindOptions = {
|
||||
type: timelineSavedObjectType,
|
||||
perPage: pageInfo != null ? pageInfo.pageSize : undefined,
|
||||
page: pageInfo != null ? pageInfo.pageIndex : undefined,
|
||||
search: search != null ? search : undefined,
|
||||
searchFields: onlyUserFavorite
|
||||
? ['title', 'description', 'favorite.keySearch']
|
||||
: ['title', 'description'],
|
||||
sortField: sort != null ? sort.sortField : undefined,
|
||||
sortOrder: sort != null ? sort.sortOrder : undefined,
|
||||
};
|
||||
return getAllSavedTimeline(request, options);
|
||||
};
|
||||
|
||||
export const persistFavorite = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null
|
||||
): Promise<ResponseFavoriteTimeline> => {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
const fullName = request.user?.full_name ?? '';
|
||||
try {
|
||||
let timeline: SavedTimeline = {};
|
||||
if (timelineId != null) {
|
||||
const {
|
||||
eventIdToNoteIds,
|
||||
notes,
|
||||
noteIds,
|
||||
pinnedEventIds,
|
||||
pinnedEventsSaveObject,
|
||||
savedObjectId,
|
||||
version,
|
||||
...savedTimeline
|
||||
} = await getBasicSavedTimeline(request, timelineId);
|
||||
timelineId = savedObjectId; // eslint-disable-line no-param-reassign
|
||||
timeline = savedTimeline;
|
||||
}
|
||||
|
||||
const userFavoriteTimeline = {
|
||||
keySearch: userName != null ? convertStringToBase64(userName) : null,
|
||||
favoriteDate: new Date().valueOf(),
|
||||
fullName,
|
||||
userName,
|
||||
};
|
||||
if (timeline.favorite != null) {
|
||||
const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex(
|
||||
user => user.userName === userName
|
||||
);
|
||||
|
||||
timeline.favorite =
|
||||
alreadyExistsTimelineFavoriteByUser > -1
|
||||
? [
|
||||
...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser),
|
||||
...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1),
|
||||
]
|
||||
: [...timeline.favorite, userFavoriteTimeline];
|
||||
} else if (timeline.favorite == null) {
|
||||
timeline.favorite = [userFavoriteTimeline];
|
||||
}
|
||||
|
||||
const persistResponse = await persistTimeline(request, timelineId, null, timeline);
|
||||
return {
|
||||
savedObjectId: persistResponse.timeline.savedObjectId,
|
||||
version: persistResponse.timeline.version,
|
||||
favorite:
|
||||
persistResponse.timeline.favorite != null
|
||||
? persistResponse.timeline.favorite.filter(fav => fav.userName === userName)
|
||||
: [],
|
||||
};
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
return {
|
||||
savedObjectId: '',
|
||||
version: '',
|
||||
favorite: [],
|
||||
code: 403,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const persistTimeline = async (
|
||||
request: FrameworkRequest,
|
||||
timelineId: string | null,
|
||||
version: string | null,
|
||||
timeline: SavedTimeline
|
||||
): Promise<ResponseTimeline> => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
try {
|
||||
if (timelineId == null) {
|
||||
// Create new timeline
|
||||
const newTimeline = convertSavedObjectToSavedTimeline(
|
||||
await savedObjectsClient.create(
|
||||
timelineSavedObjectType,
|
||||
pickSavedTimeline(timelineId, timeline, request.user)
|
||||
)
|
||||
);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
timeline: await this.getSavedTimeline(request, timelineId),
|
||||
timeline: newTimeline,
|
||||
};
|
||||
} catch (err) {
|
||||
if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) {
|
||||
return {
|
||||
code: 409,
|
||||
message: err.message,
|
||||
timeline: await this.getSavedTimeline(request, timelineId),
|
||||
};
|
||||
} else if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
const timelineToReturn: TimelineResult = {
|
||||
...timeline,
|
||||
savedObjectId: '',
|
||||
version: '',
|
||||
};
|
||||
return {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
timeline: timelineToReturn,
|
||||
};
|
||||
}
|
||||
// Update Timeline
|
||||
await savedObjectsClient.update(
|
||||
timelineSavedObjectType,
|
||||
timelineId,
|
||||
pickSavedTimeline(timelineId, timeline, request.user),
|
||||
{
|
||||
version: version || undefined,
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
await Promise.all(
|
||||
timelineIds.map(timelineId =>
|
||||
Promise.all([
|
||||
savedObjectsClient.delete(timelineSavedObjectType, timelineId),
|
||||
this.note.deleteNoteByTimelineId(request, timelineId),
|
||||
this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
|
||||
|
||||
return convertSavedObjectToSavedTimeline(savedObject);
|
||||
}
|
||||
|
||||
private async getSavedTimeline(request: FrameworkRequest, timelineId: string) {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
|
||||
const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);
|
||||
const timelineWithNotesAndPinnedEvents = await Promise.all([
|
||||
this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
Promise.resolve(timelineSaveObject),
|
||||
]);
|
||||
|
||||
const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents;
|
||||
|
||||
return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName);
|
||||
}
|
||||
|
||||
private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) {
|
||||
options.search = `${options.search != null ? options.search : ''} ${
|
||||
userName != null ? convertStringToBase64(userName) : null
|
||||
}`;
|
||||
}
|
||||
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
|
||||
const timelinesWithNotesAndPinnedEvents = await Promise.all(
|
||||
savedObjects.saved_objects.map(async savedObject => {
|
||||
const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);
|
||||
return Promise.all([
|
||||
this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
this.pinnedEvent.getAllPinnedEventsByTimelineId(
|
||||
request,
|
||||
timelineSaveObject.savedObjectId
|
||||
),
|
||||
Promise.resolve(timelineSaveObject),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: savedObjects.total,
|
||||
timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) =>
|
||||
timelineWithReduxProperties(notes, pinnedEvents, timeline, userName)
|
||||
),
|
||||
code: 200,
|
||||
message: 'success',
|
||||
timeline: await getSavedTimeline(request, timelineId),
|
||||
};
|
||||
} catch (err) {
|
||||
if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) {
|
||||
return {
|
||||
code: 409,
|
||||
message: err.message,
|
||||
timeline: await getSavedTimeline(request, timelineId),
|
||||
};
|
||||
} else if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
const timelineToReturn: TimelineResult = {
|
||||
...timeline,
|
||||
savedObjectId: '',
|
||||
version: '',
|
||||
};
|
||||
return {
|
||||
code: 403,
|
||||
message: err.message,
|
||||
timeline: timelineToReturn,
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTimeline = async (request: FrameworkRequest, timelineIds: string[]) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
|
||||
await Promise.all(
|
||||
timelineIds.map(timelineId =>
|
||||
Promise.all([
|
||||
savedObjectsClient.delete(timelineSavedObjectType, timelineId),
|
||||
note.deleteNoteByTimelineId(request, timelineId),
|
||||
pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId),
|
||||
])
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
|
||||
|
||||
return convertSavedObjectToSavedTimeline(savedObject);
|
||||
};
|
||||
|
||||
const getSavedTimeline = async (request: FrameworkRequest, timelineId: string) => {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
|
||||
const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);
|
||||
const timelineWithNotesAndPinnedEvents = await Promise.all([
|
||||
note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
Promise.resolve(timelineSaveObject),
|
||||
]);
|
||||
|
||||
const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents;
|
||||
|
||||
return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName);
|
||||
};
|
||||
|
||||
const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => {
|
||||
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
|
||||
const savedObjectsClient = request.context.core.savedObjects.client;
|
||||
if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) {
|
||||
options.search = `${options.search != null ? options.search : ''} ${
|
||||
userName != null ? convertStringToBase64(userName) : null
|
||||
}`;
|
||||
}
|
||||
|
||||
const savedObjects = await savedObjectsClient.find(options);
|
||||
|
||||
const timelinesWithNotesAndPinnedEvents = await Promise.all(
|
||||
savedObjects.saved_objects.map(async savedObject => {
|
||||
const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);
|
||||
return Promise.all([
|
||||
note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId),
|
||||
Promise.resolve(timelineSaveObject),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: savedObjects.total,
|
||||
timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) =>
|
||||
timelineWithReduxProperties(notes, pinnedEvents, timeline, userName)
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64');
|
||||
|
||||
|
@ -283,11 +331,9 @@ export const timelineWithReduxProperties = (
|
|||
timeline.favorite != null && userName != null
|
||||
? timeline.favorite.filter(fav => fav.userName === userName)
|
||||
: [],
|
||||
eventIdToNoteIds: notes.filter(note => note.eventId != null),
|
||||
noteIds: notes
|
||||
.filter(note => note.eventId == null && note.noteId != null)
|
||||
.map(note => note.noteId),
|
||||
eventIdToNoteIds: notes.filter(n => n.eventId != null),
|
||||
noteIds: notes.filter(n => n.eventId == null && n.noteId != null).map(n => n.noteId),
|
||||
notes,
|
||||
pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId),
|
||||
pinnedEventIds: pinnedEvents.map(e => e.eventId),
|
||||
pinnedEventsSaveObject: pinnedEvents,
|
||||
});
|
||||
|
|
|
@ -231,6 +231,15 @@ export const timelineSavedObjectMappings = {
|
|||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
templateTimelineId: {
|
||||
type: 'text',
|
||||
},
|
||||
templateTimelineVersion: {
|
||||
type: 'integer',
|
||||
},
|
||||
timelineType: {
|
||||
type: 'keyword',
|
||||
},
|
||||
dateRange: {
|
||||
properties: {
|
||||
start: {
|
||||
|
|
|
@ -30,6 +30,8 @@ import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/fin
|
|||
import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route';
|
||||
import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route';
|
||||
import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route';
|
||||
import { createTimelinesRoute } from '../lib/timeline/routes/create_timelines_route';
|
||||
import { updateTimelinesRoute } from '../lib/timeline/routes/update_timelines_route';
|
||||
import { SetupPlugins } from '../plugin';
|
||||
import { ConfigType } from '../config';
|
||||
|
||||
|
@ -55,6 +57,8 @@ export const initRoutes = (
|
|||
patchRulesBulkRoute(router);
|
||||
deleteRulesBulkRoute(router);
|
||||
|
||||
createTimelinesRoute(router, config, security);
|
||||
updateTimelinesRoute(router, config, security);
|
||||
importRulesRoute(router, config);
|
||||
exportRulesRoute(router, config);
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -106,6 +105,3 @@ export type ChildResolverOf<Resolver_, ParentResolver> = ResolverWithParent<
|
|||
Resolver_,
|
||||
ResultOf<ParentResolver>
|
||||
>;
|
||||
|
||||
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
|
||||
runtimeTypes.union([type, runtimeTypes.null]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue