diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts index f1307335215e..4844b0c54519 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts @@ -28,6 +28,7 @@ export interface EventsActionGroupData { export interface EventHit extends SearchHit { sort: string[]; _source: EventSource; + fields: Record; aggregations: { // eslint-disable-next-line @typescript-eslint/no-explicit-any [agg: string]: any; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts index 578f90561774..6f71ed5c2751 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts @@ -31,7 +31,7 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest { export interface TimelineRequestOptionsPaginated extends TimelineRequestBasicOptions { pagination: Pick; - sort: SortField; + sort: Array>; } export type TimelineStrategyResponseType< diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 967b3870cb9e..4b26b4157da0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -143,10 +143,15 @@ const SavedFavoriteRuntimeType = runtimeTypes.partial({ /* * Sort Types */ -const SavedSortRuntimeType = runtimeTypes.partial({ + +const SavedSortObject = runtimeTypes.partial({ columnId: unionWithNullType(runtimeTypes.string), sortDirection: unionWithNullType(runtimeTypes.string), }); +const SavedSortRuntimeType = runtimeTypes.union([ + runtimeTypes.array(SavedSortObject), + SavedSortObject, +]); /* * Timeline Statuses diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 9eb49c19c23f..664de967b9af 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -157,9 +157,9 @@ describe('Events Viewer', () => { it('re-orders columns via drag and drop', () => { const originalColumnOrder = - '@timestampmessagehost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; + '@timestamp1messagehost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; const expectedOrderAfterDragAndDrop = - 'message@timestamphost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; + 'message@timestamp1host.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; cy.get(HEADERS_GROUP).invoke('text').should('equal', originalColumnOrder); dragAndDropColumn({ column: 0, newPosition: 0 }); diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson index 9cca356a8b05..7ee8d189d7dc 100644 --- a/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson @@ -1 +1 @@ -{"savedObjectId":"0162c130-78be-11ea-9718-118a926974a4","version":"WzcsMV0=","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"}],"created":1586256805054,"createdBy":"elastic","dataProviders":[],"dateRange":{"end":1586256837669,"start":1546343624710},"description":"description","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name:*","kind":"kuery"},"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}"}},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"title":"SIEM test","updated":1586256839298,"updatedBy":"elastic","timelineType":"default","eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} +{"savedObjectId":"0162c130-78be-11ea-9718-118a926974a4","version":"WzcsMV0=","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"}],"created":1586256805054,"createdBy":"elastic","dataProviders":[],"dateRange":{"end":1586256837669,"start":1546343624710},"description":"description","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name:*","kind":"kuery"},"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}"}},"savedQueryId":null,"sort":[{"columnId":"@timestamp","sortDirection":"desc"}],"title":"SIEM test","updated":1586256839298,"updatedBy":"elastic","timelineType":"default","eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index f0eae407eedc..5aaef5cbb9ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -19,7 +19,7 @@ import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; -import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; @@ -230,7 +230,7 @@ export const useGetTimelineId = function ( if ( myElem != null && myElem.classList != null && - myElem.classList.contains(SELECTOR_TIMELINE_BODY_CLASS_NAME) && + myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && myElem.hasAttribute('data-timeline-id') ) { setTimelineId(myElem.getAttribute('data-timeline-id')); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 7132add229ed..8710503924d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -79,10 +79,12 @@ const eventsViewerDefaultProps = { language: 'kql', }, start: from, - sort: { - columnId: 'foo', - sortDirection: 'none' as SortDirection, - }, + sort: [ + { + columnId: 'foo', + sortDirection: 'none' as SortDirection, + }, + ], scopeId: SourcererScopeName.timeline, utilityBar, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 208d60ac7386..c578e017c4d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -115,7 +115,7 @@ interface Props { query: Query; onRuleChange?: () => void; start: string; - sort: Sort; + sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; @@ -202,11 +202,12 @@ const EventsViewerComponent: React.FC = ({ ]); const sortField = useMemo( - () => ({ - field: sort.columnId, - direction: sort.sortDirection as Direction, - }), - [sort.columnId, sort.sortDirection] + () => + sort.map(({ columnId, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + })), + [sort] ); const [ @@ -341,7 +342,7 @@ export const EventsViewer = React.memo( prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && prevProps.start === nextProps.start && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && prevProps.graphEventId === nextProps.graphEventId ); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index db414dfab5c0..db2184799153 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -239,7 +239,7 @@ export const mockGlobalState: State = { pinnedEventIds: {}, pinnedEventsSaveObject: {}, itemsPerPageOptions: [5, 10, 20], - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], isSaving: false, version: null, status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index d927fcb27e09..c8d9fc981d88 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2142,10 +2142,12 @@ export const mockTimelineModel: TimelineModel = { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ], status: TimelineStatus.active, title: 'Test rule', timelineType: TimelineType.default, @@ -2177,7 +2179,7 @@ export const mockTimelineResult: TimelineResult = { templateTimelineId: null, templateTimelineVersion: null, savedQueryId: null, - sort: { columnId: '@timestamp', sortDirection: 'desc' }, + sort: [{ columnId: '@timestamp', sortDirection: 'desc' }], version: '1', }; @@ -2247,7 +2249,7 @@ export const defaultTimelineProps: CreateTimelineProps = { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 55258af7332e..d251cce38153 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -105,80 +105,38 @@ describe('alert actions', () => { activeTab: TimelineTabs.query, columns: [ { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: '@timestamp', - placeholder: undefined, - type: undefined, width: 190, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'message', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'event.category', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'host.name', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'source.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'destination.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'user.name', - placeholder: undefined, - type: undefined, width: 180, }, ], @@ -242,10 +200,12 @@ describe('alert actions', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 30dec34ab39b..9e0cf10a54aa 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2231,7 +2231,7 @@ "name": "sort", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -2953,33 +2953,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "SortTimelineResult", - "description": "", - "fields": [ - { - "name": "columnId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sortDirection", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "ENUM", "name": "TimelineStatus", @@ -3650,7 +3623,15 @@ { "name": "sort", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "SortTimelineInput", "ofType": null }, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "SortTimelineInput", "ofType": null } + } + }, "defaultValue": null }, { diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 17f8e19a6055..435576a02b30 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -103,7 +103,7 @@ export interface TimelineInput { savedQueryId?: Maybe; - sort?: Maybe; + sort?: Maybe; status?: Maybe; } @@ -512,17 +512,17 @@ export interface CloudFields { machine?: Maybe; - provider?: Maybe[]>; + provider?: Maybe<(Maybe)[]>; - region?: Maybe[]>; + region?: Maybe<(Maybe)[]>; } export interface CloudInstance { - id?: Maybe[]>; + id?: Maybe<(Maybe)[]>; } export interface CloudMachine { - type?: Maybe[]>; + type?: Maybe<(Maybe)[]>; } export interface EndpointFields { @@ -632,7 +632,7 @@ export interface TimelineResult { savedObjectId: string; - sort?: Maybe; + sort?: Maybe; status?: Maybe; @@ -775,14 +775,8 @@ export interface KueryFilterQueryResult { expression?: Maybe; } -export interface SortTimelineResult { - columnId?: Maybe; - - sortDirection?: Maybe; -} - export interface ResponseTimelines { - timeline: Maybe[]; + timeline: (Maybe)[]; totalCount?: Maybe; @@ -1533,9 +1527,9 @@ export interface HostFields { id?: Maybe; - ip?: Maybe[]>; + ip?: Maybe<(Maybe)[]>; - mac?: Maybe[]>; + mac?: Maybe<(Maybe)[]>; name?: Maybe; @@ -1551,7 +1545,7 @@ export interface IndexField { /** Example of field's value */ example?: Maybe; /** whether the field's belong to an alias index */ - indexes: Maybe[]; + indexes: (Maybe)[]; /** The name of the field */ name: string; /** The type of the field's values as recognized by Kibana */ @@ -1749,7 +1743,7 @@ export namespace GetHostOverviewQuery { __typename?: 'AgentFields'; id: Maybe; - } + }; export type Host = { __typename?: 'HostEcsFields'; @@ -1788,21 +1782,21 @@ export namespace GetHostOverviewQuery { machine: Maybe; - provider: Maybe[]>; + provider: Maybe<(Maybe)[]>; - region: Maybe[]>; + region: Maybe<(Maybe)[]>; }; export type Instance = { __typename?: 'CloudInstance'; - id: Maybe[]>; + id: Maybe<(Maybe)[]>; }; export type Machine = { __typename?: 'CloudMachine'; - type: Maybe[]>; + type: Maybe<(Maybe)[]>; }; export type Inspect = { @@ -1985,7 +1979,7 @@ export namespace GetAllTimeline { favoriteCount: Maybe; - timeline: Maybe[]; + timeline: (Maybe)[]; }; export type Timeline = { @@ -2240,7 +2234,7 @@ export namespace GetOneTimeline { savedQueryId: Maybe; - sort: Maybe; + sort: Maybe; created: Maybe; @@ -2494,14 +2488,6 @@ export namespace GetOneTimeline { version: Maybe; }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe; - - sortDirection: Maybe; - }; } export namespace PersistTimelineMutation { @@ -2560,7 +2546,7 @@ export namespace PersistTimelineMutation { savedQueryId: Maybe; - sort: Maybe; + sort: Maybe; created: Maybe; @@ -2744,14 +2730,6 @@ export namespace PersistTimelineMutation { end: Maybe; }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe; - - sortDirection: Maybe; - }; } export namespace PersistTimelinePinnedEventMutation { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5a1540b97030..6c76da44c855 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -312,10 +312,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -411,10 +413,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.template, @@ -510,10 +514,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -607,10 +613,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -745,10 +753,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -912,10 +922,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -1007,10 +1019,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.immutable, title: 'Awesome Timeline', timelineType: TimelineType.template, @@ -1106,10 +1120,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, title: 'Awesome Timeline', timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 1ee529cc77a9..76eb9196e8c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -108,21 +108,20 @@ const parseString = (params: string) => { } }; -const setTimelineColumn = (col: ColumnHeaderResult) => { - const timelineCols: ColumnHeaderOptions = { - ...col, - columnHeaderType: defaultColumnHeaderType, - id: col.id != null ? col.id : 'unknown', - placeholder: col.placeholder != null ? col.placeholder : undefined, - category: col.category != null ? col.category : undefined, - description: col.description != null ? col.description : undefined, - example: col.example != null ? col.example : undefined, - type: col.type != null ? col.type : undefined, - aggregatable: col.aggregatable != null ? col.aggregatable : undefined, - width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, - }; - return timelineCols; -}; +const setTimelineColumn = (col: ColumnHeaderResult) => + Object.entries(col).reduce( + (acc, [key, value]) => { + if (key !== 'id' && value != null) { + return { ...acc, [key]: value }; + } + return acc; + }, + { + columnHeaderType: defaultColumnHeaderType, + id: col.id != null ? col.id : 'unknown', + width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, + } + ); const setTimelineFilters = (filter: FilterTimelineResult) => ({ $state: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 7772bcede76f..36e0652c3032 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx index c4c4e0e0c706..8ec8827ccbed 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -18,7 +18,7 @@ interface Props { header: ColumnHeaderOptions; isLoading: boolean; onColumnRemoved: OnColumnRemoved; - sort: Sort; + sort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -53,7 +53,7 @@ CloseButton.displayName = 'CloseButton'; export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { return ( <> - {sort.columnId === header.id && isLoading ? ( + {sort.some((i) => i.columnId === header.id) && isLoading ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx index 8bf9b6ceb346..543ffe279894 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -26,7 +26,7 @@ interface ColumneHeaderProps { header: ColumnHeaderOptions; isDragging: boolean; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -131,6 +131,6 @@ export const ColumnHeader = React.memo( prevProps.timelineId === nextProps.timelineId && prevProps.isDragging === nextProps.isDragging && prevProps.onFilterChange === nextProps.onFilterChange && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && deepEqual(prevProps.header, nextProps.header) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap index 517f537b9a01..fa9a4e78d88f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -14,10 +14,12 @@ exports[`Header renders correctly against snapshot 1`] = ` isResizing={false} onClick={[Function]} sort={ - Object { - "columnId": "@timestamp", - "sortDirection": "desc", - } + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] } > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx index 19d0220cd346..656cf234ea66 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx @@ -14,15 +14,14 @@ import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from import { Sort } from '../../sort'; import { SortIndicator } from '../../sort/sort_indicator'; import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getSortDirection } from './helpers'; - +import { getSortDirection, getSortIndex } from './helpers'; interface HeaderContentProps { children: React.ReactNode; header: ColumnHeaderOptions; isLoading: boolean; isResizing: boolean; onClick: () => void; - sort: Sort; + sort: Sort[]; } const HeaderContentComponent: React.FC = ({ @@ -33,7 +32,7 @@ const HeaderContentComponent: React.FC = ({ onClick, sort, }) => ( - + {header.aggregatable ? ( = ({ ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index 609f690903bf..b2ad186ce1b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -11,7 +11,7 @@ import { Sort, SortDirection } from '../../sort'; interface GetNewSortDirectionOnClickParams { clickedHeader: ColumnHeaderOptions; - currentSort: Sort; + currentSort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -19,7 +19,10 @@ export const getNewSortDirectionOnClick = ({ clickedHeader, currentSort, }: GetNewSortDirectionOnClickParams): Direction => - clickedHeader.id === currentSort.columnId ? getNextSortDirection(currentSort) : Direction.desc; + currentSort.reduce( + (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), + Direction.desc + ); /** Given a current sort direction, it returns the next sort direction */ export const getNextSortDirection = (currentSort: Sort): Direction => { @@ -37,8 +40,14 @@ export const getNextSortDirection = (currentSort: Sort): Direction => { interface GetSortDirectionParams { header: ColumnHeaderOptions; - sort: Sort; + sort: Sort[]; } export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - header.id === sort.columnId ? sort.sortDirection : 'none'; + sort.reduce( + (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), + 'none' + ); + +export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => + sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx index 3ef9beb89309..58d40c94ac33 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx @@ -32,10 +32,12 @@ const filteredColumnHeader: ColumnHeaderType = 'text-filter'; describe('Header', () => { const columnHeader = defaultHeaders[0]; - const sort: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; const timelineId = 'fakeId'; test('renders correctly against snapshot', () => { @@ -119,10 +121,12 @@ describe('Header', () => { expect(mockDispatch).toBeCalledWith( timelineActions.updateSort({ id: timelineId, - sort: { - columnId: columnHeader.id, - sortDirection: Direction.asc, // (because the previous state was Direction.desc) - }, + sort: [ + { + columnId: columnHeader.id, + sortDirection: Direction.asc, // (because the previous state was Direction.desc) + }, + ], }) ); }); @@ -158,7 +162,7 @@ describe('Header', () => { ); - wrapper.find('[data-test-subj="header"]').first().simulate('click'); + wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); expect(mockOnColumnSorted).not.toHaveBeenCalled(); }); @@ -180,14 +184,16 @@ describe('Header', () => { describe('getSortDirection', () => { test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort.sortDirection); + expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); }); test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort = { - columnId: 'differentSocks', - sortDirection: Direction.desc, - }; + const nonMatching: Sort[] = [ + { + columnId: 'differentSocks', + sortDirection: Direction.desc, + }, + ]; expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); }); @@ -221,10 +227,12 @@ describe('Header', () => { describe('getNewSortDirectionOnClick', () => { test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sortMatches: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; expect( getNewSortDirectionOnClick({ @@ -235,10 +243,12 @@ describe('Header', () => { }); test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort = { - columnId: 'someOtherColumn', - sortDirection: 'none', - }; + const sortDoesNotMatch: Sort[] = [ + { + columnId: 'someOtherColumn', + sortDirection: 'none', + }, + ]; expect( getNewSortDirectionOnClick({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index 15d75cc9a438..192a9c6b0973 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -21,7 +21,7 @@ import { useManageTimeline } from '../../../../manage_timeline'; interface Props { header: ColumnHeaderOptions; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -33,22 +33,39 @@ export const HeaderComponent: React.FC = ({ }) => { const dispatch = useDispatch(); - const onClick = useCallback( - () => - dispatch( - timelineActions.updateSort({ - id: timelineId, - sort: { - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), - }, - }) - ), - [dispatch, header, timelineId, sort] - ); + const onColumnSort = useCallback(() => { + const columnId = header.id; + const sortDirection = getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }); + const headerIndex = sort.findIndex((col) => col.columnId === columnId); + let newSort = []; + if (headerIndex === -1) { + newSort = [ + ...sort, + { + columnId, + sortDirection, + }, + ]; + } else { + newSort = [ + ...sort.slice(0, headerIndex), + { + columnId, + sortDirection, + }, + ...sort.slice(headerIndex + 1), + ]; + } + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: newSort, + }) + ); + }, [dispatch, header, sort, timelineId]); const onColumnRemoved = useCallback( (columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })), @@ -68,7 +85,7 @@ export const HeaderComponent: React.FC = ({ header={header} isLoading={isLoading} isResizing={false} - onClick={onClick} + onClick={onColumnSort} sort={sort} > { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +const timelineId = 'test'; describe('ColumnHeaders', () => { const mount = useMountAppended(); describe('rendering', () => { - const sort: Sort = { - columnId: 'fooColumn', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -39,7 +54,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -58,7 +73,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -78,7 +93,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -88,4 +103,145 @@ describe('ColumnHeaders', () => { }); }); }); + + describe('#onColumnsSorted', () => { + let mockSort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + let mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + + beforeEach(() => { + mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + mockSort = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + }); + + test('Add column `event.category` as desc sorting', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + { columnId: 'event.category', sortDirection: Direction.desc }, + ], + }) + ); + }); + + test('Change order of column `@timestamp` from desc to asc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.asc, + }, + { columnId: 'host.name', sortDirection: Direction.asc }, + ], + }) + ); + }); + + test('Change order of column `host.name` from asc to desc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { columnId: 'host.name', sortDirection: Direction.desc }, + ], + }) + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index aeab6a774ca4..66856f3bd628 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiCheckbox, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiDataGridSorting, + EuiToolTip, + useDataGridColumnSorting, +} from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; -import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; @@ -34,11 +42,18 @@ import { EventsThGroupData, EventsTrHeader, } from '../../styles'; -import { Sort } from '../sort'; +import { Sort, SortDirection } from '../sort'; import { EventsSelect } from './events_select'; import { ColumnHeader } from './column_header'; import * as i18n from './translations'; +import { timelineActions } from '../../../../store/timeline'; + +const SortingColumnsContainer = styled.div` + .euiPopover .euiButtonEmpty .euiButtonContent .euiButtonEmpty__text { + display: none; + } +`; interface Props { actionsColumnWidth: number; @@ -49,7 +64,7 @@ interface Props { onSelectAll: OnSelectAll; showEventsSelect: boolean; showSelectAllCheckbox: boolean; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -98,6 +113,7 @@ export const ColumnHeadersComponent = ({ sort, timelineId, }: Props) => { + const dispatch = useDispatch(); const [draggingIndex, setDraggingIndex] = useState(null); const { timelineFullScreen, @@ -189,6 +205,48 @@ export const ColumnHeadersComponent = ({ [ColumnHeaderList] ); + const myColumns = useMemo( + () => + columnHeaders.map(({ aggregatable, label, id, type }) => ({ + id, + isSortable: aggregatable, + displayAsText: label, + schema: type, + })), + [columnHeaders] + ); + + const onSortColumns = useCallback( + (cols: EuiDataGridSorting['columns']) => + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: cols.map(({ id, direction }) => ({ + columnId: id, + sortDirection: direction as SortDirection, + })), + }) + ), + [dispatch, timelineId] + ); + const sortedColumns = useMemo( + () => ({ + onSort: onSortColumns, + columns: sort.map<{ id: string; direction: 'asc' | 'desc' }>( + ({ columnId, sortDirection }) => ({ + id: columnId, + direction: sortDirection as 'asc' | 'desc', + }) + ), + }), + [onSortColumns, sort] + ); + const displayValues = useMemo( + () => columnHeaders.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.label ?? ch.id }), {}), + [columnHeaders] + ); + const ColumnSorting = useDataGridColumnSorting(myColumns, sortedColumns, {}, [], displayValues); + return ( @@ -245,6 +303,13 @@ export const ColumnHeadersComponent = ({ + + + + {ColumnSorting} + + + {showEventsSelect && ( @@ -278,7 +343,7 @@ export const ColumnHeaders = React.memo( prevProps.onSelectAll === nextProps.onSelectAll && prevProps.showEventsSelect === nextProps.showEventsSelect && prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.timelineId === nextProps.timelineId && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.browserFields, nextProps.browserFields) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts index 1ebfa957b654..c946182ddfe0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts @@ -22,6 +22,10 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS defaultMessage: 'Full screen', }); +export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', { + defaultMessage: 'Sort fields', +}); + export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', { defaultMessage: 'Type', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 6fddb5403561..bf70d7bff1ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -7,15 +7,17 @@ /** The minimum (fixed) width of the Actions column */ export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; + /** The (fixed) width of the Actions column */ -export const DEFAULT_ACTIONS_COLUMN_WIDTH = 24 * 4; // px; +export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px; /** * The (fixed) width of the Actions column when the timeline body is used as * an events viewer, which has fewer actions than a regular events viewer */ -export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 24 * 3; // px; -/** Additional column width to include when checkboxes are shown **/ -export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; +export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px; + /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index fc9967bdeff9..704af61b4a12 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -16,13 +16,14 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; -import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../styles'; import { timelineActions } from '../../../store/timeline'; -const mockSort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, -}; +const mockSort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, +]; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -130,20 +131,6 @@ describe('Body', () => { }); }); }, 20000); - - test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_BODY_CLASS_NAME}`, () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`) - .first() - .exists() - ).toEqual(true); - }); }); describe('action on event', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 45641a34f2cf..ea397b67c31c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -33,7 +33,7 @@ interface OwnProps { data: TimelineItem[]; id: string; isEventViewer?: boolean; - sort: Sort; + sort: Sort[]; refetch: inputsModel.Refetch; onRuleChange?: () => void; } @@ -144,7 +144,7 @@ export const BodyComponent = React.memo( return ( <> - + ); + } else if (fieldType === GEO_FIELD_TYPE) { + return <>{value}; } else if (fieldType === DATE_FIELD_TYPE) { return ( + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx index dcaedb90e725..6593abf71e36 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx @@ -15,12 +15,12 @@ import { getDirection, SortIndicator } from './sort_indicator'; describe('SortIndicator', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortUp' @@ -28,7 +28,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortDown' @@ -36,7 +36,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'empty' @@ -60,7 +60,7 @@ describe('SortIndicator', () => { describe('sort indicator tooltip', () => { test('it returns the expected tooltip when the direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -68,7 +68,7 @@ describe('SortIndicator', () => { }); test('it returns the expected tooltip when the direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -76,7 +76,7 @@ describe('SortIndicator', () => { }); test('it does NOT render a tooltip when sort direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx index 8b842dfa2197..518103e8cb64 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Direction } from '../../../../../graphql/types'; import * as i18n from '../translations'; +import { SortNumber } from './sort_number'; import { SortDirection } from '.'; @@ -35,10 +36,11 @@ export const getDirection = (sortDirection: SortDirection): SortDirectionIndicat interface Props { sortDirection: SortDirection; + sortNumber: number; } /** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection }) => { +export const SortIndicator = React.memo(({ sortDirection, sortNumber }) => { const direction = getDirection(sortDirection); if (direction != null) { @@ -51,7 +53,10 @@ export const SortIndicator = React.memo(({ sortDirection }) => { } data-test-subj="sort-indicator-tooltip" > - + <> + + + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx new file mode 100644 index 000000000000..48dd70a16e70 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import React from 'react'; + +interface Props { + sortNumber: number; +} + +export const SortNumber = React.memo(({ sortNumber }) => { + if (sortNumber >= 0) { + return ( + + {sortNumber + 1} + + ); + } else { + return ; + } +}); + +SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts index 54755fbc8427..11bc3da8c05b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts @@ -38,6 +38,10 @@ export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => /** Invoked when a column is sorted */ export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + export type OnColumnRemoved = (columnId: ColumnId) => void; export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 085a9bf8cba3..59a7b936dfba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -20,6 +20,7 @@ import { import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -56,7 +57,7 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { - timelineId: 'id', + timelineId: 'timeline-test', }; beforeEach(() => { @@ -71,4 +72,18 @@ describe('StatefulTimeline', () => { ); expect(wrapper.find('[data-test-subj="timeline"]')).toBeTruthy(); }); + + test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`, () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) + .first() + .exists() + ).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 37145b9348ac..4e6bca7fd962 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -21,13 +21,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h import { activeTimeline } from '../../containers/active_timeline_context'; import * as i18n from './translations'; import { TabsContent } from './tabs_content'; - -const TimelineContainer = styled.div` - height: 100%; - display: flex; - flex-direction: column; - position: relative; -`; +import { TimelineContainer } from './styles'; const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; @@ -78,7 +72,7 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { }, []); return ( - + {timelineType === TimelineType.template && ( {i18n.TIMELINE_TEMPLATE} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index c726e92455f2..c9355797193a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,10 +276,12 @@ In other use cases the message field can be used to concatenate different values showCallOutUnauthorizedMsg={false} showEventDetails={false} sort={ - Object { - "columnId": "@timestamp", - "sortDirection": "desc", - } + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] } start="2018-03-23T18:49:23.132Z" status="active" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 4019f46b8c07..7e60461a0157 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -64,10 +64,12 @@ jest.mock('../../../../common/lib/kibana', () => { describe('Timeline', () => { let props = {} as QueryTabContentComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 8186ee8b7762..69a7299b9833 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -214,11 +214,12 @@ export const QueryTabContentComponent: React.FC = ({ }, [columns]); const timelineQuerySortField = useMemo( - () => ({ - field: sort.columnId, - direction: sort.sortDirection as Direction, - }), - [sort.columnId, sort.sortDirection] + () => + sort.map(({ columnId, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + })), + [sort] ); const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 9f9940203960..ef7c821bd652 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -11,6 +11,19 @@ import styled, { createGlobalStyle } from 'styled-components'; import { TimelineEventsType } from '../../../../common/types/timeline'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; +/** + * TIMELINE BODY + */ +export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; +export const TimelineContainer = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + height: 100%; + display: flex; + flex-direction: column; + position: relative; +`; + /** * TIMELINE BODY */ @@ -99,6 +112,9 @@ export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ min-width: 0; padding-left: ${({ isEventViewer }) => !isEventViewer ? '4px;' : '0;'}; // match timeline event border + button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } `; export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index a439699d27f6..7e2a6fa1c15c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -73,10 +73,12 @@ const timelineData = { end: 1591084965409, }, savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, }; const mockPatchTimelineResponse = { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 8f1644550d14..ebc86b3c5cf5 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -85,6 +85,9 @@ export const useTimelineEventsDetails = ({ } }, error: () => { + if (!didCancel) { + setLoading(false); + } notifications.toasts.addDanger('Failed to run search'); }, }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index a168e814208e..3baab2024558 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -56,7 +56,7 @@ export interface UseTimelineEventsProps { fields: string[]; indexNames: string[]; limit: number; - sort: SortField; + sort: SortField[]; startDate: string; timerangeKind?: 'absolute' | 'relative'; } @@ -65,10 +65,12 @@ const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => timelineEdges.map((e: TimelineEdges) => e.node); const ID = 'timelineEventsQuery'; -export const initSortDefault = { - field: '@timestamp', - direction: Direction.asc, -}; +export const initSortDefault = [ + { + field: '@timestamp', + direction: Direction.asc, + }, +]; function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) { const ref = useRef(value); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 1a09868da777..604767bcde26 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -32,7 +32,12 @@ export const getTimelinesInStorageByIds = (storage: Storage, timelineIds: Timeli return { ...acc, - [timelineId]: timelineModel, + [timelineId]: { + ...timelineModel, + ...(timelineModel.sort != null && !Array.isArray(timelineModel.sort) + ? { sort: [timelineModel.sort] } + : {}), + }, }; }, {} as { [K in TimelineIdLiteral]: TimelineModel }); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index fa0ecb349f9c..9e34d3470d29 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -138,10 +138,7 @@ export const oneTimelineQuery = gql` templateTimelineId templateTimelineVersion savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts index 12d3e6bfd717..e255ac5bdda5 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts @@ -102,10 +102,7 @@ export const persistTimelineMutation = gql` end } savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index b8dfa698a930..479c289cdd21 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -72,7 +72,7 @@ export interface TimelineInput { filterQuery: SerializedFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineType?: TimelineTypeLiteral; templateTimelineId?: string | null; @@ -216,7 +216,7 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin 'UPDATE_RANGE' ); -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); +export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT'); export const updateAutoSaveMsg = actionCreator<{ timelineId: string | null; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 84551de9ec62..211bba3cc47d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -52,10 +52,12 @@ export const timelineDefaults: SubsetTimelineModel & Pick { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], status: TimelineStatus.active, version: 'WzM4LDFd', id: '11169110-fc22-11e9-8ca9-072f15ce2685', @@ -286,10 +286,12 @@ describe('Epic Timeline', () => { }, }, savedQueryId: 'my endgame timeline query', - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], templateTimelineId: null, templateTimelineVersion: null, timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index a2bccaddb309..5fcbcf434d3e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -58,10 +58,12 @@ describe('epicLocalStorage', () => { ); let props = {} as QueryTabContentComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; @@ -159,10 +161,12 @@ describe('epicLocalStorage', () => { store.dispatch( updateSort({ id: 'test', - sort: { - columnId: 'event.severity', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'event.severity', + sortDirection: Direction.desc, + }, + ], }) ); await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 1122b7a94e0e..f9f4622c9d63 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -179,7 +179,7 @@ interface AddNewTimelineParams { filterQuery: SerializedFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineById: TimelineById; timelineType: TimelineTypeLiteral; @@ -762,7 +762,7 @@ export const updateTimelineRange = ({ interface UpdateTimelineSortParams { id: string; - sort: Sort; + sort: Sort[]; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index e4d1a6b51268..9c71fabfffac 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -124,7 +124,7 @@ export interface TimelineModel { /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ showCheckboxes: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; + sort: Sort[]; /** status: active | draft */ status: TimelineStatus; /** updated saved object timestamp */ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 2ca34742affe..59d5800271b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -101,10 +101,12 @@ const basicTimeline: TimelineModel = { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ], status: TimelineStatus.active, templateTimelineId: null, templateTimelineVersion: null, @@ -953,10 +955,12 @@ describe('Timeline', () => { beforeAll(() => { update = updateTimelineSort({ id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'some column', + sortDirection: Direction.desc, + }, + ], timelineById: timelineByIdMock, }); }); @@ -964,8 +968,8 @@ describe('Timeline', () => { expect(update).not.toBe(timelineByIdMock); }); - test('should update the timeline range', () => { - expect(update.foo.sort).toEqual({ columnId: 'some column', sortDirection: Direction.desc }); + test('should update the sort attribute', () => { + expect(update.foo.sort).toEqual([{ columnId: 'some column', sortDirection: Direction.desc }]); }); }); diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 2358e78b044e..ca6c57f025fa 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -174,7 +174,7 @@ export const timelineSchema = gql` timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String - sort: SortTimelineInput + sort: [SortTimelineInput!] status: TimelineStatus } @@ -238,10 +238,6 @@ export const timelineSchema = gql` ${favoriteTimeline} } - type SortTimelineResult { - ${sortTimeline} - } - type FilterMetaTimelineResult { ${filtersMetaTimeline} } @@ -277,7 +273,7 @@ export const timelineSchema = gql` pinnedEventsSaveObject: [PinnedEvent!] savedQueryId: String savedObjectId: String! - sort: SortTimelineResult + sort: ToAny status: TimelineStatus title: String templateTimelineId: String diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index bda0fed494a6..3ea964c0ee01 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -105,7 +105,7 @@ export interface TimelineInput { savedQueryId?: Maybe; - sort?: Maybe; + sort?: Maybe; status?: Maybe; } @@ -634,7 +634,7 @@ export interface TimelineResult { savedObjectId: string; - sort?: Maybe; + sort?: Maybe; status?: Maybe; @@ -777,12 +777,6 @@ export interface KueryFilterQueryResult { expression?: Maybe; } -export interface SortTimelineResult { - columnId?: Maybe; - - sortDirection?: Maybe; -} - export interface ResponseTimelines { timeline: (Maybe)[]; @@ -2336,7 +2330,6 @@ export namespace AgentFieldsResolvers { > = Resolver; } - export namespace CloudFieldsResolvers { export interface Resolvers { instance?: InstanceResolver, TypeParent, TContext>; @@ -2665,7 +2658,7 @@ export namespace TimelineResultResolvers { savedObjectId?: SavedObjectIdResolver; - sort?: SortResolver, TypeParent, TContext>; + sort?: SortResolver, TypeParent, TContext>; status?: StatusResolver, TypeParent, TContext>; @@ -2785,7 +2778,7 @@ export namespace TimelineResultResolvers { TContext = SiemContext > = Resolver; export type SortResolver< - R = Maybe, + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -3245,25 +3238,6 @@ export namespace KueryFilterQueryResultResolvers { > = Resolver; } -export namespace SortTimelineResultResolvers { - export interface Resolvers { - columnId?: ColumnIdResolver, TypeParent, TContext>; - - sortDirection?: SortDirectionResolver, TypeParent, TContext>; - } - - export type ColumnIdResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; - export type SortDirectionResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; -} - export namespace ResponseTimelinesResolvers { export interface Resolvers { timeline?: TimelineResolver<(Maybe)[], TypeParent, TContext>; @@ -6091,7 +6065,6 @@ export type IResolvers = { SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; - SortTimelineResult?: SortTimelineResultResolvers.Resolvers; ResponseTimelines?: ResponseTimelinesResolvers.Resolvers; Mutation?: MutationResolvers.Resolvers; ResponseNote?: ResponseNoteResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index f888675b6041..271e53d4e6c9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -60,6 +60,12 @@ export const convertSavedObjectToSavedTimeline = (savedObject: unknown): Timelin savedTimeline.attributes.timelineType, savedTimeline.attributes.status ), + sort: + savedTimeline.attributes.sort != null + ? Array.isArray(savedTimeline.attributes.sort) + ? savedTimeline.attributes.sort + : [savedTimeline.attributes.sort] + : [], }; return { savedObjectId: savedTimeline.id, diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts index 1aba6660677c..9fd371c6f1cc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -7,5 +7,37 @@ export const toArray = (value: T | T[] | null): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; -export const toStringArray = (value: T | T[] | null): T[] | string[] => - Array.isArray(value) ? value : value == null ? [] : [`${value}`]; +export const toStringArray = (value: T | T[] | null): string[] => { + if (Array.isArray(value)) { + return value.reduce((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(value)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts new file mode 100644 index 000000000000..b62ddc00f2e3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -0,0 +1,123 @@ +/* + * 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 { EventHit } from '../../../../../../common/search_strategy'; +import { TIMELINE_EVENTS_FIELDS } from './constants'; +import { formatTimelineData } from './helpers'; + +describe('#formatTimelineData', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect( + formatTimelineData( + ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], + TIMELINE_EVENTS_FIELDS, + response + ) + ).toEqual({ + cursor: { + tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', + value: '1605624488922', + }, + node: { + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + data: [ + { + field: '@timestamp', + value: ['2020-11-17T14:48:08.922Z'], + }, + { + field: 'host.name', + value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + ], + ecs: { + '@timestamp': ['2020-11-17T14:48:08.922Z'], + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + agent: { + type: ['auditbeat'], + }, + event: { + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + module: ['system'], + type: ['start'], + }, + host: { + id: ['e59991e835905c65ed3e455b33e13bd6'], + ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + os: { + family: ['debian'], + }, + }, + message: ['Process go (PID: 4313) by user jenkins STARTED'], + process: { + args: ['go', 'vet', './...'], + entity_id: ['Z59cIkAAIw8ZoK0H'], + executable: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + hash: { + sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + name: ['go'], + pid: ['4313'], + ppid: ['3977'], + working_directory: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + timestamp: '2020-11-17T14:48:08.922Z', + user: { + name: ['jenkins'], + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index 8e2bfb542661..a9aee2175b31 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -7,6 +7,7 @@ import { get, has, merge, uniq } from 'lodash/fp'; import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; +import { formatGeoLocation, isGeoField } from '../details/helpers'; export const formatTimelineData = ( dataFields: readonly string[], @@ -18,7 +19,7 @@ export const formatTimelineData = ( flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; - flattenedFields.node.ecs.timestamp = hit._source['@timestamp']; + flattenedFields.node.ecs.timestamp = (hit.fields['@timestamp'][0] ?? '') as string; flattenedFields.node.ecs._index = hit._index; if (hit.sort && hit.sort.length > 1) { flattenedFields.cursor.value = hit.sort[0]; @@ -40,13 +41,12 @@ const specialFields = ['_id', '_index', '_type', '_score']; const mergeTimelineFieldsWithHit = ( fieldName: string, flattenedFields: T, - hit: { _source: {} }, + hit: { fields: Record }, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { - const esField = fieldName; - if (has(esField, hit._source) || specialFields.includes(esField)) { + if (has(fieldName, hit.fields) || specialFields.includes(fieldName)) { const objectWithProperty = { node: { ...get('node', flattenedFields), @@ -55,9 +55,11 @@ const mergeTimelineFieldsWithHit = ( ...get('node.data', flattenedFields), { field: fieldName, - value: specialFields.includes(esField) - ? toStringArray(get(esField, hit)) - : toStringArray(get(esField, hit._source)), + value: specialFields.includes(fieldName) + ? toStringArray(get(fieldName, hit)) + : isGeoField(fieldName) + ? formatGeoLocation(hit.fields[fieldName]) + : toStringArray(hit.fields[fieldName]), }, ] : get('node.data', flattenedFields), @@ -68,7 +70,7 @@ const mergeTimelineFieldsWithHit = ( ...fieldName.split('.').reduceRight( // @ts-expect-error (obj, next) => ({ [next]: obj }), - toStringArray(get(esField, hit._source)) + toStringArray(hit.fields[fieldName]) ), } : get('node.ecs', flattenedFields), diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 19535fa3dc8a..de58e7cf44d6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -9,11 +9,12 @@ import { cloneDeep, uniq } from 'lodash/fp'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { + EventHit, TimelineEventsQueries, TimelineEventsAllStrategyResponse, TimelineEventsAllRequestOptions, TimelineEdges, -} from '../../../../../../common/search_strategy/timeline'; +} from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineEventsAllQuery } from './query.events_all.dsl'; @@ -39,8 +40,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory - // @ts-expect-error - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit) + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) ); const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index a5a0c877ecdd..034a2b3c6ea9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -44,14 +44,11 @@ export const buildTimelineEventsAllQuery = ({ const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; - const getSortField = (sortField: SortField) => { - if (sortField.field) { - const field: string = sortField.field === 'timestamp' ? '@timestamp' : sortField.field; - - return [{ [field]: sortField.direction }]; - } - return []; - }; + const getSortField = (sortFields: SortField[]) => + sortFields.map((item) => { + const field: string = item.field === 'timestamp' ? '@timestamp' : item.field; + return { [field]: item.direction }; + }); const dslQuery = { allowNoIndices: true, @@ -68,7 +65,7 @@ export const buildTimelineEventsAllQuery = ({ size: querySize, track_total_hits: true, sort: getSortField(sort), - _source: fields, + fields, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts new file mode 100644 index 000000000000..34610da7d7aa --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { EventHit } from '../../../../../../common/search_strategy'; +import { getDataFromHits } from './helpers'; + +describe('#getDataFromHits', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect(getDataFromHits(response.fields)).toEqual([ + { + category: 'event', + field: 'event.category', + originalValue: ['process'], + values: ['process'], + }, + { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] }, + { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] }, + { + category: 'process', + field: 'process.args', + originalValue: ['go', 'vet', './...'], + values: ['go', 'vet', './...'], + }, + { + category: 'base', + field: 'message', + originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], + values: ['Process go (PID: 4313) by user jenkins STARTED'], + }, + { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] }, + { + category: 'process', + field: 'process.working_directory', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + { + category: 'process', + field: 'process.entity_id', + originalValue: ['Z59cIkAAIw8ZoK0H'], + values: ['Z59cIkAAIw8ZoK0H'], + }, + { + category: 'host', + field: 'host.ip', + originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + }, + { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] }, + { + category: 'event', + field: 'event.action', + originalValue: ['process_started'], + values: ['process_started'], + }, + { + category: 'agent', + field: 'agent.type', + originalValue: ['auditbeat'], + values: ['auditbeat'], + }, + { + category: 'base', + field: '@timestamp', + originalValue: ['2020-11-17T14:48:08.922Z'], + values: ['2020-11-17T14:48:08.922Z'], + }, + { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] }, + { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] }, + { + category: 'host', + field: 'host.name', + originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + category: 'process', + field: 'process.hash.sha1', + originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] }, + { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] }, + { + category: 'host', + field: 'host.id', + originalValue: ['e59991e835905c65ed3e455b33e13bd6'], + values: ['e59991e835905c65ed3e455b33e13bd6'], + }, + { + category: 'event', + field: 'event.dataset', + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.executable', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 2dd406ffaa45..68bef2e8c656 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy/timeline'; +import { toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -18,39 +19,32 @@ export const getFieldCategory = (field: string): string => { return fieldCategory; }; -export const getDataFromHits = ( - sources: EventSource, - category?: string, - path?: string -): TimelineEventsDetailsItem[] => - Object.keys(sources).reduce((accumulator, source) => { - const item: EventSource = get(source, sources); - if (Array.isArray(item) || isString(item) || isNumber(item)) { - const field = path ? `${path}.${source}` : source; - const fieldCategory = getFieldCategory(field); - - return [ - ...accumulator, - { - category: fieldCategory, - field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, - } as TimelineEventsDetailsItem, - ]; - } else if (isObject(item)) { - return [ - ...accumulator, - ...getDataFromHits(item, category || source, path ? `${path}.${source}` : source), - ]; +export const formatGeoLocation = (item: unknown[]) => { + const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; + if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { + try { + return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] }); + } catch { + return toStringArray(item); } - return accumulator; + } + return toStringArray(item); +}; + +export const isGeoField = (field: string) => + field.includes('geo.location') || field.includes('geoip.location'); + +export const getDataFromHits = (fields: Record): TimelineEventsDetailsItem[] => + Object.keys(fields).reduce((accumulator, field) => { + const item: unknown[] = fields[field]; + const fieldCategory = getFieldCategory(field); + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item), + originalValue: toStringArray(item), + } as TimelineEventsDetailsItem, + ]; }, []); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 54e138c1e9d6..0a011d2bfe87 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, merge } from 'lodash/fp'; +import { cloneDeep, merge } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -27,13 +27,14 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const sourceData = getOr({}, 'hits.hits.0._source', response.rawResponse); - const hitsData = getOr({}, 'hits.hits.0', response.rawResponse); + const fieldsData = cloneDeep(response.rawResponse.hits.hits[0].fields ?? {}); + const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); delete hitsData._source; + delete hitsData.fields; const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - const data = getDataFromHits(merge(sourceData, hitsData)); + const data = getDataFromHits(merge(fieldsData, hitsData)); return { ...response, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 216e8f947d26..8d70a08c214d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -21,6 +21,7 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, + fields: ['*'], }, size: 1, }); diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index a399c07e3106..07e7cad89c24 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -171,7 +171,7 @@ export default function ({ getService }: FtrProviderContext) { expect(kqlMode).to.be(timelineObject.kqlMode); expect(kqlQuery).to.eql(timelineObject.kqlQuery); expect(savedObjectId).to.not.be.empty(); - expect(sort).to.eql(timelineObject.sort); + expect(sort).to.eql([timelineObject.sort]); expect(title).to.be(timelineObject.title); expect(version).to.not.be.empty(); }); diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index d3f40188aa6d..a04c2fef9232 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -14,228 +14,211 @@ import { FtrProviderContext } from '../../ftr_provider_context'; const INDEX_NAME = 'filebeat-7.0.0-iot-2019.06'; const ID = 'QRhG1WgBqd-n62SwZYDT'; const EXPECTED_DATA = [ - { - category: 'base', - field: '@timestamp', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', - }, - { category: '@version', field: '@version', values: ['1'], originalValue: '1' }, - { - category: 'agent', - field: 'agent.ephemeral_id', - values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', - }, - { - category: 'agent', - field: 'agent.hostname', - values: ['raspberrypi'], - originalValue: 'raspberrypi', - }, - { - category: 'agent', - field: 'agent.id', - values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', - }, - { category: 'agent', field: 'agent.type', values: ['filebeat'], originalValue: 'filebeat' }, - { category: 'agent', field: 'agent.version', values: ['7.0.0'], originalValue: '7.0.0' }, - { - category: 'destination', - field: 'destination.domain', - values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', - }, - { - category: 'destination', - field: 'destination.ip', - values: ['10.100.7.196'], - originalValue: '10.100.7.196', - }, - { category: 'destination', field: 'destination.port', values: [40684], originalValue: 40684 }, - { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], originalValue: '1.0.0-beta2' }, - { - category: 'event', - field: 'event.dataset', - values: ['suricata.eve'], - originalValue: 'suricata.eve', - }, - { - category: 'event', - field: 'event.end', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', - }, - { category: 'event', field: 'event.kind', values: ['event'], originalValue: 'event' }, - { category: 'event', field: 'event.module', values: ['suricata'], originalValue: 'suricata' }, - { category: 'event', field: 'event.type', values: ['fileinfo'], originalValue: 'fileinfo' }, { category: 'file', field: 'file.path', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], }, - { category: 'file', field: 'file.size', values: [48277], originalValue: 48277 }, - { category: 'fileset', field: 'fileset.name', values: ['eve'], originalValue: 'eve' }, - { category: 'flow', field: 'flow.locality', values: ['public'], originalValue: 'public' }, - { category: 'host', field: 'host.architecture', values: ['armv7l'], originalValue: 'armv7l' }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { - category: 'host', - field: 'host.id', - values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', - }, - { category: 'host', field: 'host.name', values: ['raspberrypi'], originalValue: 'raspberrypi' }, - { category: 'host', field: 'host.os.codename', values: ['stretch'], originalValue: 'stretch' }, - { category: 'host', field: 'host.os.family', values: [''], originalValue: '' }, - { - category: 'host', - field: 'host.os.kernel', - values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + category: 'traefik', + field: 'traefik.access.geoip.location', + values: ['{"long":-122.3341,"lat":47.6103}'], + originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], }, { - category: 'host', - field: 'host.os.name', - values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + category: 'suricata', + field: 'suricata.eve.src_port', + values: ['80'], + originalValue: ['80'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + }, + { + category: 'service', + field: 'service.type', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'http', + field: 'http.request.method', + values: ['get'], + originalValue: ['get'], }, - { category: 'host', field: 'host.os.platform', values: ['raspbian'], originalValue: 'raspbian' }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', - }, - { category: 'http', field: 'http.request.method', values: ['get'], originalValue: 'get' }, - { category: 'http', field: 'http.response.body.bytes', values: [48277], originalValue: 48277 }, - { category: 'http', field: 'http.response.status_code', values: [206], originalValue: 206 }, - { category: 'input', field: 'input.type', values: ['log'], originalValue: 'log' }, - { - category: 'base', - field: 'labels.pipeline', - values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', - }, - { - category: 'log', - field: 'log.file.path', - values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', - }, - { category: 'log', field: 'log.offset', values: [1856288115], originalValue: 1856288115 }, - { category: 'network', field: 'network.name', values: ['iot'], originalValue: 'iot' }, - { category: 'network', field: 'network.protocol', values: ['http'], originalValue: 'http' }, - { category: 'network', field: 'network.transport', values: ['tcp'], originalValue: 'tcp' }, - { category: 'service', field: 'service.type', values: ['suricata'], originalValue: 'suricata' }, - { category: 'source', field: 'source.as.num', values: [16509], originalValue: 16509 }, - { - category: 'source', - field: 'source.as.org', - values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', - }, - { - category: 'source', - field: 'source.domain', - values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', - }, - { - category: 'source', - field: 'source.geo.city_name', - values: ['Seattle'], - originalValue: 'Seattle', - }, - { - category: 'source', - field: 'source.geo.continent_name', - values: ['North America'], - originalValue: 'North America', - }, - { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], originalValue: 'US' }, - { - category: 'source', - field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, - }, - { - category: 'source', - field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, - }, - { - category: 'source', - field: 'source.geo.region_iso_code', - values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['9 (stretch)'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', - }, - { - category: 'source', - field: 'source.ip', - values: ['54.239.219.210'], - originalValue: '54.239.219.210', - }, - { category: 'source', field: 'source.port', values: [80], originalValue: 80 }, - { - category: 'suricata', - field: 'suricata.eve.fileinfo.state', - values: ['CLOSED'], - originalValue: 'CLOSED', - }, - { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', values: [301], originalValue: 301 }, - { - category: 'suricata', - field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, - }, - { - category: 'suricata', - field: 'suricata.eve.http.http_content_type', - values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['Washington'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, - { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], originalValue: 'eth0' }, - { category: 'base', field: 'tags', values: ['suricata'], originalValue: ['suricata'] }, { - category: 'url', - field: 'url.domain', + category: 'host', + field: 'host.os.name', + values: ['Raspbian GNU/Linux'], + originalValue: ['Raspbian GNU/Linux'], + }, + { + category: 'source', + field: 'source.ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + }, + { + category: 'host', + field: 'host.name', + values: ['raspberrypi'], + originalValue: ['raspberrypi'], + }, + { + category: 'source', + field: 'source.geo.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], + }, + { + category: 'http', + field: 'http.response.status_code', + values: ['206'], + originalValue: ['206'], + }, + { + category: 'event', + field: 'event.kind', + values: ['event'], + originalValue: ['event'], + }, + { + category: 'suricata', + field: 'suricata.eve.flow_id', + values: ['196625917175466'], + originalValue: ['196625917175466'], + }, + { + category: 'source', + field: 'source.geo.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + }, + { + category: 'suricata', + field: 'suricata.eve.proto', + values: ['tcp'], + originalValue: ['tcp'], + }, + { + category: 'flow', + field: 'flow.locality', + values: ['public'], + originalValue: ['public'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.country_iso_code', + values: ['US'], + originalValue: ['US'], + }, + { + category: 'fileset', + field: 'fileset.name', + values: ['eve'], + originalValue: ['eve'], + }, + { + category: 'input', + field: 'input.type', + values: ['log'], + originalValue: ['log'], + }, + { + category: 'log', + field: 'log.offset', + values: ['1856288115'], + originalValue: ['1856288115'], + }, + { + category: 'destination', + field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { - category: 'url', - field: 'url.original', + category: 'agent', + field: 'agent.hostname', + values: ['raspberrypi'], + originalValue: ['raspberrypi'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.hostname', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + }, + { + category: 'suricata', + field: 'suricata.eve.in_iface', + values: ['eth0'], + originalValue: ['eth0'], + }, + { + category: 'base', + field: 'tags', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'host', + field: 'host.architecture', + values: ['armv7l'], + originalValue: ['armv7l'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.status', + values: ['206'], + originalValue: ['206'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.url', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -243,22 +226,336 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'source', + field: 'source.port', + values: ['80'], + originalValue: ['80'], + }, + { + category: 'agent', + field: 'agent.id', + values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + }, + { + category: 'host', + field: 'host.containerized', + values: ['false'], + originalValue: ['false'], + }, + { + category: 'ecs', + field: 'ecs.version', + values: ['1.0.0-beta2'], + originalValue: ['1.0.0-beta2'], + }, + { + category: 'agent', + field: 'agent.version', + values: ['7.0.0'], + originalValue: ['7.0.0'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.stored', + values: ['false'], + originalValue: ['false'], + }, + { + category: 'host', + field: 'host.os.family', + values: [''], + originalValue: [''], + }, + { + category: 'base', + field: 'labels.pipeline', + values: ['filebeat-7.0.0-suricata-eve-pipeline'], + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], + }, + { + category: 'suricata', + field: 'suricata.eve.src_ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.state', + values: ['CLOSED'], + originalValue: ['CLOSED'], + }, + { + category: 'destination', + field: 'destination.port', + values: ['40684'], + originalValue: ['40684'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_name', + values: ['Washington'], + originalValue: ['Washington'], + }, + { + category: 'source', + field: 'source.as.num', + values: ['16509'], + originalValue: ['16509'], + }, + { + category: 'event', + field: 'event.end', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + }, + { + category: 'source', + field: 'source.geo.location', + values: ['{"long":-122.3341,"lat":47.6103}'], + originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], + }, + { + category: 'source', + field: 'source.domain', + values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.size', + values: ['48277'], + originalValue: ['48277'], + }, + { + category: 'suricata', + field: 'suricata.eve.app_proto', + values: ['http'], + originalValue: ['http'], + }, + { + category: 'agent', + field: 'agent.type', + values: ['filebeat'], + originalValue: ['filebeat'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.tx_id', + values: ['301'], + originalValue: ['301'], + }, + { + category: 'event', + field: 'event.module', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'network', + field: 'network.protocol', + values: ['http'], + originalValue: ['http'], + }, + { + category: 'host', + field: 'host.os.kernel', + values: ['4.14.50-v7+'], + originalValue: ['4.14.50-v7+'], + }, + { + category: 'source', + field: 'source.geo.country_iso_code', + values: ['US'], + originalValue: ['US'], + }, + { + category: '@version', + field: '@version', + values: ['1'], + originalValue: ['1'], + }, + { + category: 'host', + field: 'host.id', + values: ['b19a781f683541a7a25ee345133aa399'], + originalValue: ['b19a781f683541a7a25ee345133aa399'], + }, + { + category: 'source', + field: 'source.as.org', + values: ['Amazon.com, Inc.'], + originalValue: ['Amazon.com, Inc.'], + }, + { + category: 'suricata', + field: 'suricata.eve.timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + }, + { + category: 'host', + field: 'host.os.codename', + values: ['stretch'], + originalValue: ['stretch'], + }, + { + category: 'source', + field: 'source.geo.continent_name', + values: ['North America'], + originalValue: ['North America'], + }, + { + category: 'network', + field: 'network.name', + values: ['iot'], + originalValue: ['iot'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.http_method', + values: ['get'], + originalValue: ['get'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.continent_name', + values: ['North America'], + originalValue: ['North America'], + }, + { + category: 'file', + field: 'file.size', + values: ['48277'], + originalValue: ['48277'], + }, + { + category: 'destination', + field: 'destination.ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.length', + values: ['48277'], + originalValue: ['48277'], + }, + { + category: 'http', + field: 'http.response.body.bytes', + values: ['48277'], + originalValue: ['48277'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.filename', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'suricata', + field: 'suricata.eve.dest_ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], + }, + { + category: 'network', + field: 'network.transport', + values: ['tcp'], + originalValue: ['tcp'], + }, + { + category: 'url', + field: 'url.original', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'base', + field: '@timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + }, + { + category: 'host', + field: 'host.os.platform', + values: ['raspbian'], + originalValue: ['raspbian'], + }, + { + category: 'suricata', + field: 'suricata.eve.dest_port', + values: ['40684'], + originalValue: ['40684'], + }, + { + category: 'event', + field: 'event.type', + values: ['fileinfo'], + originalValue: ['fileinfo'], + }, + { + category: 'log', + field: 'log.file.path', + values: ['/var/log/suricata/eve.json'], + originalValue: ['/var/log/suricata/eve.json'], + }, + { + category: 'url', + field: 'url.domain', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + }, + { + category: 'agent', + field: 'agent.ephemeral_id', + values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.http_content_type', + values: ['video/mp4'], + originalValue: ['video/mp4'], + }, + { + category: 'event', + field: 'event.dataset', + values: ['suricata.eve'], + originalValue: ['suricata.eve'], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], + }, + { + category: '_score', + field: '_score', + values: ['1'], + originalValue: ['1'], }, - { category: '_score', field: '_score', values: [1], originalValue: 1 }, ]; export default function ({ getService }: FtrProviderContext) {