[Security Solution] Fix sorting on unmapped fields in Timeline Events… (#87241)

* [Security Solution] Fix sorting on unmapped fields in Timeline Events table

* set unmapped_type to the column type

* add missing types

* Update saved_object_mappings.ts

Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
Patryk Kopyciński 2021-01-12 00:09:38 +01:00 committed by GitHub
parent 7cd1fa3167
commit 1e7c3f88ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 121 additions and 27 deletions

View file

@ -28,10 +28,14 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest {
factoryQueryType?: TimelineFactoryQueryTypes;
}
export interface TimelineRequestSortField<Field = string> extends SortField<Field> {
type: string;
}
export interface TimelineRequestOptionsPaginated<Field = string>
extends TimelineRequestBasicOptions {
pagination: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
sort: Array<SortField<Field>>;
sort: Array<TimelineRequestSortField<Field>>;
}
export type TimelineStrategyResponseType<

View file

@ -146,6 +146,7 @@ const SavedFavoriteRuntimeType = runtimeTypes.partial({
const SavedSortObject = runtimeTypes.partial({
columnId: unionWithNullType(runtimeTypes.string),
columnType: unionWithNullType(runtimeTypes.string),
sortDirection: unionWithNullType(runtimeTypes.string),
});
const SavedSortRuntimeType = runtimeTypes.union([

View file

@ -104,6 +104,7 @@ const eventsViewerDefaultProps = {
sort: [
{
columnId: 'foo',
columnType: 'number',
sortDirection: 'asc' as SortDirection,
},
],

View file

@ -216,8 +216,9 @@ const EventsViewerComponent: React.FC<Props> = ({
const sortField = useMemo(
() =>
sort.map(({ columnId, sortDirection }) => ({
sort.map(({ columnId, columnType, sortDirection }) => ({
field: columnId,
type: columnType,
direction: sortDirection as Direction,
})),
[sort]

View file

@ -26,6 +26,11 @@ SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper';
and `EuiPopover`, `EuiToolTip` global styles
*/
export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>`
// fixes double scrollbar on views with EventsTable
#kibana-body {
overflow: hidden;
}
div.app-wrapper {
background-color: rgba(0,0,0,0);
}

View file

@ -238,7 +238,7 @@ export const mockGlobalState: State = {
pinnedEventIds: {},
pinnedEventsSaveObject: {},
itemsPerPageOptions: [5, 10, 20],
sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }],
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }],
isSaving: false,
version: null,
status: TimelineStatus.active,

View file

@ -2150,6 +2150,7 @@ export const mockTimelineModel: TimelineModel = {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
],
@ -2184,7 +2185,7 @@ export const mockTimelineResult: TimelineResult = {
templateTimelineId: null,
templateTimelineVersion: null,
savedQueryId: null,
sort: [{ columnId: '@timestamp', sortDirection: 'desc' }],
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' }],
version: '1',
};
@ -2202,7 +2203,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
timeline: {
activeTab: TimelineTabs.query,
columns: [
{ columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 },
{ columnHeaderType: 'not-filtered', id: '@timestamp', type: 'number', width: 190 },
{ columnHeaderType: 'not-filtered', id: 'message', width: 180 },
{ columnHeaderType: 'not-filtered', id: 'event.category', width: 180 },
{ columnHeaderType: 'not-filtered', id: 'event.action', width: 180 },
@ -2254,7 +2255,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
selectedEventIds: {},
show: false,
showCheckboxes: false,
sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }],
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }],
status: TimelineStatus.draft,
title: '',
timelineType: TimelineType.default,

View file

@ -111,6 +111,7 @@ describe('alert actions', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -207,6 +208,7 @@ describe('alert actions', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],

View file

@ -151,6 +151,7 @@ export const mockTimeline = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
__typename: 'SortTimelineResult',
},
@ -403,6 +404,7 @@ export const mockTemplate = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
__typename: 'SortTimelineResult',
},

View file

@ -246,6 +246,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -319,6 +320,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -347,6 +349,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -420,6 +423,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -448,6 +452,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -521,6 +526,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -547,6 +553,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -620,6 +627,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -652,7 +660,7 @@ describe('helpers', () => {
example: undefined,
id: '@timestamp',
placeholder: undefined,
type: undefined,
type: 'number',
width: 190,
},
{
@ -760,6 +768,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -815,6 +824,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -929,6 +939,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -953,6 +964,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -1026,6 +1038,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],
@ -1054,6 +1067,7 @@ describe('helpers', () => {
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'number',
width: 190,
},
{
@ -1127,6 +1141,7 @@ describe('helpers', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],

View file

@ -421,6 +421,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
Object {
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"type": "number",
"width": 190,
},
Object {
@ -468,6 +469,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
Array [
Object {
"columnId": "@timestamp",
"columnType": "number",
"sortDirection": "desc",
},
]

View file

@ -13,6 +13,7 @@ export const defaultHeaders: ColumnHeaderOptions[] = [
{
columnHeaderType: defaultColumnHeaderType,
id: '@timestamp',
type: 'number',
width: DEFAULT_DATE_COLUMN_MIN_WIDTH,
},
{

View file

@ -7,6 +7,7 @@ exports[`Header renders correctly against snapshot 1`] = `
Object {
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"type": "number",
"width": 190,
}
}
@ -17,6 +18,7 @@ exports[`Header renders correctly against snapshot 1`] = `
Array [
Object {
"columnId": "@timestamp",
"columnType": "number",
"sortDirection": "desc",
},
]
@ -27,6 +29,7 @@ exports[`Header renders correctly against snapshot 1`] = `
Object {
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"type": "number",
"width": 190,
}
}
@ -36,6 +39,7 @@ exports[`Header renders correctly against snapshot 1`] = `
Array [
Object {
"columnId": "@timestamp",
"columnType": "number",
"sortDirection": "desc",
},
]
@ -47,6 +51,7 @@ exports[`Header renders correctly against snapshot 1`] = `
Object {
"columnHeaderType": "not-filtered",
"id": "@timestamp",
"type": "number",
"width": 190,
}
}

View file

@ -35,6 +35,7 @@ describe('Header', () => {
const sort: Sort[] = [
{
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.desc,
},
];
@ -124,6 +125,7 @@ describe('Header', () => {
sort: [
{
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.asc, // (because the previous state was Direction.desc)
},
],
@ -191,6 +193,7 @@ describe('Header', () => {
const nonMatching: Sort[] = [
{
columnId: 'differentSocks',
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.desc,
},
];
@ -201,7 +204,11 @@ describe('Header', () => {
describe('getNextSortDirection', () => {
test('it returns "asc" when the current direction is "desc"', () => {
const sortDescending: Sort = { columnId: columnHeader.id, sortDirection: Direction.desc };
const sortDescending: Sort = {
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.desc,
};
expect(getNextSortDirection(sortDescending)).toEqual('asc');
});
@ -209,6 +216,7 @@ describe('Header', () => {
test('it returns "desc" when the current direction is "asc"', () => {
const sortAscending: Sort = {
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.asc,
};
@ -218,6 +226,7 @@ describe('Header', () => {
test('it returns "desc" by default', () => {
const sortNone: Sort = {
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: 'none',
};
@ -230,6 +239,7 @@ describe('Header', () => {
const sortMatches: Sort[] = [
{
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
sortDirection: Direction.desc,
},
];
@ -246,6 +256,7 @@ describe('Header', () => {
const sortDoesNotMatch: Sort[] = [
{
columnId: 'someOtherColumn',
columnType: columnHeader.type ?? 'number',
sortDirection: 'none',
},
];

View file

@ -35,6 +35,7 @@ export const HeaderComponent: React.FC<Props> = ({
const onColumnSort = useCallback(() => {
const columnId = header.id;
const columnType = header.type ?? 'text';
const sortDirection = getNewSortDirectionOnClick({
clickedHeader: header,
currentSort: sort,
@ -46,6 +47,7 @@ export const HeaderComponent: React.FC<Props> = ({
...sort,
{
columnId,
columnType,
sortDirection,
},
];
@ -54,6 +56,7 @@ export const HeaderComponent: React.FC<Props> = ({
...sort.slice(0, headerIndex),
{
columnId,
columnType,
sortDirection,
},
...sort.slice(headerIndex + 1),

View file

@ -38,6 +38,7 @@ describe('ColumnHeaders', () => {
const sort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
];
@ -108,10 +109,12 @@ describe('ColumnHeaders', () => {
let mockSort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'text',
sortDirection: Direction.asc,
},
];
@ -126,10 +129,12 @@ describe('ColumnHeaders', () => {
mockSort = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'text',
sortDirection: Direction.asc,
},
];
@ -162,13 +167,15 @@ describe('ColumnHeaders', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'text',
sortDirection: Direction.asc,
},
{ columnId: 'event.category', sortDirection: Direction.desc },
{ columnId: 'event.category', columnType: 'text', sortDirection: Direction.desc },
],
})
);
@ -201,9 +208,10 @@ describe('ColumnHeaders', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.asc,
},
{ columnId: 'host.name', sortDirection: Direction.asc },
{ columnId: 'host.name', columnType: 'text', sortDirection: Direction.asc },
],
})
);
@ -236,9 +244,10 @@ describe('ColumnHeaders', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
{ columnId: 'host.name', sortDirection: Direction.desc },
{ columnId: 'host.name', columnType: 'text', sortDirection: Direction.desc },
],
})
);

View file

@ -230,11 +230,12 @@ export const ColumnHeadersComponent = ({
id: timelineId,
sort: cols.map(({ id, direction }) => ({
columnId: id,
columnType: columnHeaders.find((ch) => ch.id === id)?.type ?? 'text',
sortDirection: direction as SortDirection,
})),
})
),
[dispatch, timelineId]
[columnHeaders, dispatch, timelineId]
);
const sortedColumns = useMemo(
() => ({

View file

@ -22,6 +22,7 @@ import { TimelineTabs } from '../../../../../common/types/timeline';
const mockSort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
];

View file

@ -13,5 +13,6 @@ export type SortDirection = 'none' | Direction;
/** Specifies which column the timeline is sorted on */
export interface Sort {
columnId: ColumnId;
columnType: string;
sortDirection: SortDirection;
}

View file

@ -140,6 +140,7 @@ In other use cases the message field can be used to concatenate different values
Array [
Object {
"columnId": "@timestamp",
"columnType": "number",
"sortDirection": "desc",
},
]

View file

@ -66,6 +66,7 @@ describe('PinnedTabContent', () => {
const sort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
];

View file

@ -143,8 +143,9 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
const timelineQuerySortField = useMemo(
() =>
sort.map(({ columnId, sortDirection }) => ({
sort.map(({ columnId, columnType, sortDirection }) => ({
field: columnId,
type: columnType,
direction: sortDirection as Direction,
})),
[sort]

View file

@ -283,6 +283,7 @@ In other use cases the message field can be used to concatenate different values
Array [
Object {
"columnId": "@timestamp",
"columnType": "number",
"sortDirection": "desc",
},
]

View file

@ -67,6 +67,7 @@ describe('Timeline', () => {
const sort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
];

View file

@ -211,9 +211,10 @@ export const QueryTabContentComponent: React.FC<Props> = ({
const timelineQuerySortField = useMemo(
() =>
sort.map(({ columnId, sortDirection }) => ({
sort.map(({ columnId, columnType, sortDirection }) => ({
field: columnId,
direction: sortDirection as Direction,
type: columnType,
})),
[sort]
);

View file

@ -26,7 +26,7 @@ import {
TimelineEventsAllRequestOptions,
TimelineEdges,
TimelineItem,
SortField,
TimelineRequestSortField,
} from '../../../common/search_strategy';
import { InspectResponse } from '../../types';
import * as i18n from './translations';
@ -56,7 +56,7 @@ export interface UseTimelineEventsProps {
fields: string[];
indexNames: string[];
limit: number;
sort: SortField[];
sort: TimelineRequestSortField[];
startDate: string;
timerangeKind?: 'absolute' | 'relative';
}
@ -69,6 +69,7 @@ export const initSortDefault = [
{
field: '@timestamp',
direction: Direction.asc,
type: 'number',
},
];

View file

@ -55,6 +55,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
],

View file

@ -149,7 +149,7 @@ describe('Epic Timeline', () => {
selectedEventIds: {},
show: true,
showCheckboxes: false,
sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }],
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }],
status: TimelineStatus.active,
version: 'WzM4LDFd',
id: '11169110-fc22-11e9-8ca9-072f15ce2685',
@ -289,6 +289,7 @@ describe('Epic Timeline', () => {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
],

View file

@ -61,6 +61,7 @@ describe('epicLocalStorage', () => {
const sort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
];
@ -168,6 +169,7 @@ describe('epicLocalStorage', () => {
sort: [
{
columnId: 'event.severity',
columnType: 'number',
sortDirection: Direction.desc,
},
],

View file

@ -103,6 +103,7 @@ const basicTimeline: TimelineModel = {
sort: [
{
columnId: '@timestamp',
columnType: 'number',
sortDirection: Direction.desc,
},
],
@ -932,6 +933,7 @@ describe('Timeline', () => {
sort: [
{
columnId: 'some column',
columnType: 'text',
sortDirection: Direction.desc,
},
],
@ -943,7 +945,9 @@ describe('Timeline', () => {
});
test('should update the sort attribute', () => {
expect(update.foo.sort).toEqual([{ columnId: 'some column', sortDirection: Direction.desc }]);
expect(update.foo.sort).toEqual([
{ columnId: 'some column', columnType: 'text', sortDirection: Direction.desc },
]);
});
});

View file

@ -42,7 +42,7 @@ describe('pickSavedTimeline', () => {
templateTimelineVersion: null,
eventType: 'all',
filters: [],
sort: { sortDirection: 'desc', columnId: '@timestamp' },
sort: { sortDirection: 'desc', columnType: 'number', columnId: '@timestamp' },
title: 'title',
kqlMode: 'filter',
timelineType: TimelineType.default,

View file

@ -212,6 +212,6 @@ export const mockTimeline = {
templateTimelineId: null,
dateRange: { start: '2020-11-03T13:34:40.339Z', end: '2020-11-04T13:34:40.339Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
status: 'draft',
};

View file

@ -23,7 +23,7 @@ export const mockParsedObjects = [
title: 'My duplicate timeline',
dateRange: { start: '2020-03-18T09:31:47.294Z', end: '2020-03-19T09:31:47.294Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
created: 1584828930463,
createdBy: 'angela',
updated: 1584868346013,
@ -698,6 +698,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
created: 1588162404153,
@ -870,6 +871,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
timelineType: 'template',
@ -1052,6 +1054,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
timelineType: 'template',
@ -1172,6 +1175,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = {
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
timelineType: 'template',

View file

@ -73,7 +73,7 @@ export const inputTimeline: SavedTimeline = {
templateTimelineVersion: 1,
dateRange: { start: '2020-03-26T12:50:05.527Z', end: '2020-03-27T12:50:05.527Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
};
export const inputTemplateTimeline = {
@ -289,7 +289,7 @@ export const mockTimelines = () => ({
title: 'test no.2',
dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
created: 1582625382448,
createdBy: 'elastic',
updated: 1583741197521,
@ -371,7 +371,7 @@ export const mockTimelines = () => ({
title: 'test no.3',
dateRange: { start: '2020-02-24T10:09:11.145Z', end: '2020-02-25T10:09:11.145Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
created: 1582642817439,
createdBy: 'elastic',
updated: 1583741175216,

View file

@ -266,10 +266,14 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = {
type: 'keyword',
},
sort: {
dynamic: false,
properties: {
columnId: {
type: 'keyword',
},
columnType: {
type: 'keyword',
},
sortDirection: {
type: 'keyword',
},

View file

@ -6,10 +6,10 @@
import { isEmpty } from 'lodash/fp';
import {
SortField,
TimerangeFilter,
TimerangeInput,
TimelineEventsAllRequestOptions,
TimelineRequestSortField,
} from '../../../../../../common/search_strategy';
import { createQueryFilterClauses } from '../../../../../utils/build_query';
@ -46,10 +46,15 @@ export const buildTimelineEventsAllQuery = ({
const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }];
const getSortField = (sortFields: SortField[]) =>
const getSortField = (sortFields: TimelineRequestSortField[]) =>
sortFields.map((item) => {
const field: string = item.field === 'timestamp' ? '@timestamp' : item.field;
return { [field]: item.direction };
return {
[field]: {
order: item.direction,
unmapped_type: item.type,
},
};
});
const dslQuery = {