[SECURITY SOLUTION] Bundles _source -> Fields + able to sort on multiple fields in Timeline (#83761)
* replace _source with fields * wip * unit test * regroup sorting and number together * fix bugs from review * mistake * Update x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com> * fix snapshot * review + fix topN and filter from detail view * fix tests * fix test Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
parent
c9b5ec7303
commit
0f408041b4
|
@ -28,6 +28,7 @@ export interface EventsActionGroupData {
|
|||
export interface EventHit extends SearchHit {
|
||||
sort: string[];
|
||||
_source: EventSource;
|
||||
fields: Record<string, unknown[]>;
|
||||
aggregations: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[agg: string]: any;
|
||||
|
|
|
@ -31,7 +31,7 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest {
|
|||
export interface TimelineRequestOptionsPaginated<Field = string>
|
||||
extends TimelineRequestBasicOptions {
|
||||
pagination: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
|
||||
sort: SortField<Field>;
|
||||
sort: Array<SortField<Field>>;
|
||||
}
|
||||
|
||||
export type TimelineStrategyResponseType<
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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":[]}
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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<Props> = ({
|
|||
]);
|
||||
|
||||
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
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
},
|
||||
{
|
||||
|
|
|
@ -103,7 +103,7 @@ export interface TimelineInput {
|
|||
|
||||
savedQueryId?: Maybe<string>;
|
||||
|
||||
sort?: Maybe<SortTimelineInput>;
|
||||
sort?: Maybe<SortTimelineInput[]>;
|
||||
|
||||
status?: Maybe<TimelineStatus>;
|
||||
}
|
||||
|
@ -512,17 +512,17 @@ export interface CloudFields {
|
|||
|
||||
machine?: Maybe<CloudMachine>;
|
||||
|
||||
provider?: Maybe<Maybe<string>[]>;
|
||||
provider?: Maybe<(Maybe<string>)[]>;
|
||||
|
||||
region?: Maybe<Maybe<string>[]>;
|
||||
region?: Maybe<(Maybe<string>)[]>;
|
||||
}
|
||||
|
||||
export interface CloudInstance {
|
||||
id?: Maybe<Maybe<string>[]>;
|
||||
id?: Maybe<(Maybe<string>)[]>;
|
||||
}
|
||||
|
||||
export interface CloudMachine {
|
||||
type?: Maybe<Maybe<string>[]>;
|
||||
type?: Maybe<(Maybe<string>)[]>;
|
||||
}
|
||||
|
||||
export interface EndpointFields {
|
||||
|
@ -632,7 +632,7 @@ export interface TimelineResult {
|
|||
|
||||
savedObjectId: string;
|
||||
|
||||
sort?: Maybe<SortTimelineResult>;
|
||||
sort?: Maybe<ToAny>;
|
||||
|
||||
status?: Maybe<TimelineStatus>;
|
||||
|
||||
|
@ -775,14 +775,8 @@ export interface KueryFilterQueryResult {
|
|||
expression?: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface SortTimelineResult {
|
||||
columnId?: Maybe<string>;
|
||||
|
||||
sortDirection?: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface ResponseTimelines {
|
||||
timeline: Maybe<TimelineResult>[];
|
||||
timeline: (Maybe<TimelineResult>)[];
|
||||
|
||||
totalCount?: Maybe<number>;
|
||||
|
||||
|
@ -1533,9 +1527,9 @@ export interface HostFields {
|
|||
|
||||
id?: Maybe<string>;
|
||||
|
||||
ip?: Maybe<Maybe<string>[]>;
|
||||
ip?: Maybe<(Maybe<string>)[]>;
|
||||
|
||||
mac?: Maybe<Maybe<string>[]>;
|
||||
mac?: Maybe<(Maybe<string>)[]>;
|
||||
|
||||
name?: Maybe<string>;
|
||||
|
||||
|
@ -1551,7 +1545,7 @@ export interface IndexField {
|
|||
/** Example of field's value */
|
||||
example?: Maybe<string>;
|
||||
/** whether the field's belong to an alias index */
|
||||
indexes: Maybe<string>[];
|
||||
indexes: (Maybe<string>)[];
|
||||
/** 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<string>;
|
||||
}
|
||||
};
|
||||
|
||||
export type Host = {
|
||||
__typename?: 'HostEcsFields';
|
||||
|
@ -1788,21 +1782,21 @@ export namespace GetHostOverviewQuery {
|
|||
|
||||
machine: Maybe<Machine>;
|
||||
|
||||
provider: Maybe<Maybe<string>[]>;
|
||||
provider: Maybe<(Maybe<string>)[]>;
|
||||
|
||||
region: Maybe<Maybe<string>[]>;
|
||||
region: Maybe<(Maybe<string>)[]>;
|
||||
};
|
||||
|
||||
export type Instance = {
|
||||
__typename?: 'CloudInstance';
|
||||
|
||||
id: Maybe<Maybe<string>[]>;
|
||||
id: Maybe<(Maybe<string>)[]>;
|
||||
};
|
||||
|
||||
export type Machine = {
|
||||
__typename?: 'CloudMachine';
|
||||
|
||||
type: Maybe<Maybe<string>[]>;
|
||||
type: Maybe<(Maybe<string>)[]>;
|
||||
};
|
||||
|
||||
export type Inspect = {
|
||||
|
@ -1985,7 +1979,7 @@ export namespace GetAllTimeline {
|
|||
|
||||
favoriteCount: Maybe<number>;
|
||||
|
||||
timeline: Maybe<Timeline>[];
|
||||
timeline: (Maybe<Timeline>)[];
|
||||
};
|
||||
|
||||
export type Timeline = {
|
||||
|
@ -2240,7 +2234,7 @@ export namespace GetOneTimeline {
|
|||
|
||||
savedQueryId: Maybe<string>;
|
||||
|
||||
sort: Maybe<Sort>;
|
||||
sort: Maybe<ToAny>;
|
||||
|
||||
created: Maybe<number>;
|
||||
|
||||
|
@ -2494,14 +2488,6 @@ export namespace GetOneTimeline {
|
|||
|
||||
version: Maybe<string>;
|
||||
};
|
||||
|
||||
export type Sort = {
|
||||
__typename?: 'SortTimelineResult';
|
||||
|
||||
columnId: Maybe<string>;
|
||||
|
||||
sortDirection: Maybe<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace PersistTimelineMutation {
|
||||
|
@ -2560,7 +2546,7 @@ export namespace PersistTimelineMutation {
|
|||
|
||||
savedQueryId: Maybe<string>;
|
||||
|
||||
sort: Maybe<Sort>;
|
||||
sort: Maybe<ToAny>;
|
||||
|
||||
created: Maybe<number>;
|
||||
|
||||
|
@ -2744,14 +2730,6 @@ export namespace PersistTimelineMutation {
|
|||
|
||||
end: Maybe<ToAny>;
|
||||
};
|
||||
|
||||
export type Sort = {
|
||||
__typename?: 'SortTimelineResult';
|
||||
|
||||
columnId: Maybe<string>;
|
||||
|
||||
sortDirection: Maybe<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace PersistTimelinePinnedEventMutation {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ColumnHeaderOptions>(
|
||||
(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: {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={96}
|
||||
actionsColumnWidth={120}
|
||||
browserFields={
|
||||
Object {
|
||||
"agent": Object {
|
||||
|
@ -465,10 +465,12 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={
|
||||
Object {
|
||||
"columnId": "fooColumn",
|
||||
"sortDirection": "desc",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
timelineId="test"
|
||||
/>
|
||||
|
|
|
@ -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<Props>(({ header, onColumnRemoved, sort, isLoading }) => {
|
||||
return (
|
||||
<>
|
||||
{sort.columnId === header.id && isLoading ? (
|
||||
{sort.some((i) => i.columnId === header.id) && isLoading ? (
|
||||
<EventsHeadingExtra className="siemEventsHeading__extra--loading">
|
||||
<EventsLoading data-test-subj="timeline-loading-spinner" />
|
||||
</EventsHeadingExtra>
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Actions
|
||||
|
@ -31,10 +33,12 @@ exports[`Header renders correctly against snapshot 1`] = `
|
|||
isLoading={false}
|
||||
onColumnRemoved={[Function]}
|
||||
sort={
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Memo(HeaderContentComponent)>
|
||||
|
|
|
@ -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<HeaderContentProps> = ({
|
||||
|
@ -33,7 +32,7 @@ const HeaderContentComponent: React.FC<HeaderContentProps> = ({
|
|||
onClick,
|
||||
sort,
|
||||
}) => (
|
||||
<EventsHeading data-test-subj="header" isLoading={isLoading}>
|
||||
<EventsHeading data-test-subj={`header-${header.id}`} isLoading={isLoading}>
|
||||
{header.aggregatable ? (
|
||||
<EventsHeadingTitleButton
|
||||
data-test-subj="header-sort-button"
|
||||
|
@ -51,6 +50,7 @@ const HeaderContentComponent: React.FC<HeaderContentProps> = ({
|
|||
<SortIndicator
|
||||
data-test-subj="header-sort-indicator"
|
||||
sortDirection={getSortDirection({ header, sort })}
|
||||
sortNumber={getSortIndex({ header, sort })}
|
||||
/>
|
||||
</EventsHeadingTitleButton>
|
||||
) : (
|
||||
|
|
|
@ -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<Direction>(
|
||||
(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<SortDirection>(
|
||||
(acc, item) => (header.id === item.columnId ? item.sortDirection : acc),
|
||||
'none'
|
||||
);
|
||||
|
||||
export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number =>
|
||||
sort.findIndex((s) => s.columnId === header.id);
|
||||
|
|
|
@ -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', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
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({
|
||||
|
|
|
@ -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<Props> = ({
|
|||
}) => {
|
||||
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<Props> = ({
|
|||
header={header}
|
||||
isLoading={isLoading}
|
||||
isResizing={false}
|
||||
onClick={onClick}
|
||||
onClick={onColumnSort}
|
||||
sort={sort}
|
||||
>
|
||||
<Actions
|
||||
|
|
|
@ -17,15 +17,30 @@ import { TestProviders } from '../../../../../common/mock/test_providers';
|
|||
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
|
||||
|
||||
import { ColumnHeadersComponent } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
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}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -58,7 +73,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -78,7 +93,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
timelineId={'test'}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -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(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={mockDefaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onSelectAll={jest.fn}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={mockDefaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onSelectAll={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={mockDefaultHeaders}
|
||||
isSelectAllChecked={false}
|
||||
onSelectAll={jest.fn()}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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 },
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<number | null>(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 (
|
||||
<EventsThead data-test-subj="column-headers">
|
||||
<EventsTrHeader>
|
||||
|
@ -245,6 +303,13 @@ export const ColumnHeadersComponent = ({
|
|||
</EuiToolTip>
|
||||
</EventsThContent>
|
||||
</EventsTh>
|
||||
<EventsTh>
|
||||
<EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
|
||||
<EuiToolTip content={i18n.SORT_FIELDS}>
|
||||
<SortingColumnsContainer>{ColumnSorting}</SortingColumnsContainer>
|
||||
</EuiToolTip>
|
||||
</EventsThContent>
|
||||
</EventsTh>
|
||||
|
||||
{showEventsSelect && (
|
||||
<EventsTh>
|
||||
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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` */
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`)
|
||||
.first()
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action on event', () => {
|
||||
|
|
|
@ -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<StatefulBodyProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
<TimelineBody data-test-subj="timeline-body" data-timeline-id={id}>
|
||||
<TimelineBody data-test-subj="timeline-body">
|
||||
<EventsTable data-test-subj="events-table" columnWidths={columnWidths}>
|
||||
<ColumnHeaders
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export const DATE_FIELD_TYPE = 'date';
|
||||
export const HOST_NAME_FIELD_NAME = 'host.name';
|
||||
export const IP_FIELD_TYPE = 'ip';
|
||||
export const GEO_FIELD_TYPE = 'geo_point';
|
||||
export const MESSAGE_FIELD_NAME = 'message';
|
||||
export const EVENT_MODULE_FIELD_NAME = 'event.module';
|
||||
export const RULE_REFERENCE_FIELD_NAME = 'rule.reference';
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
EVENT_URL_FIELD_NAME,
|
||||
GEO_FIELD_TYPE,
|
||||
} from './constants';
|
||||
import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers';
|
||||
|
||||
|
@ -57,6 +58,8 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
truncate={truncate}
|
||||
/>
|
||||
);
|
||||
} else if (fieldType === GEO_FIELD_TYPE) {
|
||||
return <>{value}</>;
|
||||
} else if (fieldType === DATE_FIELD_TYPE) {
|
||||
return (
|
||||
<DefaultDraggable
|
||||
|
|
|
@ -11,5 +11,8 @@ exports[`SortIndicator rendering renders correctly against snapshot 1`] = `
|
|||
data-test-subj="sortIndicator"
|
||||
type="sortDown"
|
||||
/>
|
||||
<SortNumber
|
||||
sortNumber={-1}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
`;
|
||||
|
|
|
@ -15,12 +15,12 @@ import { getDirection, SortIndicator } from './sort_indicator';
|
|||
describe('SortIndicator', () => {
|
||||
describe('rendering', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<SortIndicator sortDirection={Direction.desc} />);
|
||||
const wrapper = shallow(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the expected sort indicator when direction is ascending', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} />);
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
|
||||
|
||||
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(<SortIndicator sortDirection={Direction.desc} />);
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
|
||||
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(<SortIndicator sortDirection="none" />);
|
||||
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
|
||||
|
||||
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(<SortIndicator sortDirection={Direction.asc} />);
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
|
||||
|
||||
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(<SortIndicator sortDirection={Direction.desc} />);
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
|
||||
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(<SortIndicator sortDirection="none" />);
|
||||
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false);
|
||||
});
|
||||
|
|
|
@ -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<Props>(({ sortDirection }) => {
|
||||
export const SortIndicator = React.memo<Props>(({ sortDirection, sortNumber }) => {
|
||||
const direction = getDirection(sortDirection);
|
||||
|
||||
if (direction != null) {
|
||||
|
@ -51,7 +53,10 @@ export const SortIndicator = React.memo<Props>(({ sortDirection }) => {
|
|||
}
|
||||
data-test-subj="sort-indicator-tooltip"
|
||||
>
|
||||
<EuiIcon data-test-subj="sortIndicator" type={direction} />
|
||||
<>
|
||||
<EuiIcon data-test-subj="sortIndicator" type={direction} />
|
||||
<SortNumber sortNumber={sortNumber} />
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -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<Props>(({ sortNumber }) => {
|
||||
if (sortNumber >= 0) {
|
||||
return (
|
||||
<EuiNotificationBadge color="subdued" data-test-subj="sortNumber">
|
||||
{sortNumber + 1}
|
||||
</EuiNotificationBadge>
|
||||
);
|
||||
} else {
|
||||
return <EuiIcon data-test-subj="sortEmptyNumber" type={'empty'} />;
|
||||
}
|
||||
});
|
||||
|
||||
SortNumber.displayName = 'SortNumber';
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<StatefulTimeline {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`)
|
||||
.first()
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Props> = ({ timelineId }) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<TimelineContainer data-test-subj="timeline">
|
||||
<TimelineContainer data-test-subj="timeline" data-timeline-id={timelineId}>
|
||||
<TimelineSavingProgress timelineId={timelineId} />
|
||||
{timelineType === TimelineType.template && (
|
||||
<TimelineTemplateBadge>{i18n.TIMELINE_TEMPLATE}</TimelineTemplateBadge>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -214,11 +214,12 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
}, [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();
|
||||
|
|
|
@ -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 = '' }) => ({
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -85,6 +85,9 @@ export const useTimelineEventsDetails = ({
|
|||
}
|
||||
},
|
||||
error: () => {
|
||||
if (!didCancel) {
|
||||
setLoading(false);
|
||||
}
|
||||
notifications.toasts.addDanger('Failed to run search');
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<TimelineEventsAllRequestOptions | null>(value);
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -138,10 +138,7 @@ export const oneTimelineQuery = gql`
|
|||
templateTimelineId
|
||||
templateTimelineVersion
|
||||
savedQueryId
|
||||
sort {
|
||||
columnId
|
||||
sortDirection
|
||||
}
|
||||
sort
|
||||
created
|
||||
createdBy
|
||||
updated
|
||||
|
|
|
@ -102,10 +102,7 @@ export const persistTimelineMutation = gql`
|
|||
end
|
||||
}
|
||||
savedQueryId
|
||||
sort {
|
||||
columnId
|
||||
sortDirection
|
||||
}
|
||||
sort
|
||||
created
|
||||
createdBy
|
||||
updated
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -52,10 +52,12 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
selectedEventIds: {},
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
sort: {
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
],
|
||||
status: TimelineStatus.draft,
|
||||
version: null,
|
||||
};
|
||||
|
|
|
@ -149,7 +149,7 @@ describe('Epic Timeline', () => {
|
|||
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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -105,7 +105,7 @@ export interface TimelineInput {
|
|||
|
||||
savedQueryId?: Maybe<string>;
|
||||
|
||||
sort?: Maybe<SortTimelineInput>;
|
||||
sort?: Maybe<SortTimelineInput[]>;
|
||||
|
||||
status?: Maybe<TimelineStatus>;
|
||||
}
|
||||
|
@ -634,7 +634,7 @@ export interface TimelineResult {
|
|||
|
||||
savedObjectId: string;
|
||||
|
||||
sort?: Maybe<SortTimelineResult>;
|
||||
sort?: Maybe<ToAny>;
|
||||
|
||||
status?: Maybe<TimelineStatus>;
|
||||
|
||||
|
@ -777,12 +777,6 @@ export interface KueryFilterQueryResult {
|
|||
expression?: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface SortTimelineResult {
|
||||
columnId?: Maybe<string>;
|
||||
|
||||
sortDirection?: Maybe<string>;
|
||||
}
|
||||
|
||||
export interface ResponseTimelines {
|
||||
timeline: (Maybe<TimelineResult>)[];
|
||||
|
||||
|
@ -2336,7 +2330,6 @@ export namespace AgentFieldsResolvers {
|
|||
> = Resolver<R, Parent, TContext>;
|
||||
}
|
||||
|
||||
|
||||
export namespace CloudFieldsResolvers {
|
||||
export interface Resolvers<TContext = SiemContext, TypeParent = CloudFields> {
|
||||
instance?: InstanceResolver<Maybe<CloudInstance>, TypeParent, TContext>;
|
||||
|
@ -2665,7 +2658,7 @@ export namespace TimelineResultResolvers {
|
|||
|
||||
savedObjectId?: SavedObjectIdResolver<string, TypeParent, TContext>;
|
||||
|
||||
sort?: SortResolver<Maybe<SortTimelineResult>, TypeParent, TContext>;
|
||||
sort?: SortResolver<Maybe<ToAny>, TypeParent, TContext>;
|
||||
|
||||
status?: StatusResolver<Maybe<TimelineStatus>, TypeParent, TContext>;
|
||||
|
||||
|
@ -2785,7 +2778,7 @@ export namespace TimelineResultResolvers {
|
|||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type SortResolver<
|
||||
R = Maybe<SortTimelineResult>,
|
||||
R = Maybe<ToAny>,
|
||||
Parent = TimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
|
@ -3245,25 +3238,6 @@ export namespace KueryFilterQueryResultResolvers {
|
|||
> = Resolver<R, Parent, TContext>;
|
||||
}
|
||||
|
||||
export namespace SortTimelineResultResolvers {
|
||||
export interface Resolvers<TContext = SiemContext, TypeParent = SortTimelineResult> {
|
||||
columnId?: ColumnIdResolver<Maybe<string>, TypeParent, TContext>;
|
||||
|
||||
sortDirection?: SortDirectionResolver<Maybe<string>, TypeParent, TContext>;
|
||||
}
|
||||
|
||||
export type ColumnIdResolver<
|
||||
R = Maybe<string>,
|
||||
Parent = SortTimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
export type SortDirectionResolver<
|
||||
R = Maybe<string>,
|
||||
Parent = SortTimelineResult,
|
||||
TContext = SiemContext
|
||||
> = Resolver<R, Parent, TContext>;
|
||||
}
|
||||
|
||||
export namespace ResponseTimelinesResolvers {
|
||||
export interface Resolvers<TContext = SiemContext, TypeParent = ResponseTimelines> {
|
||||
timeline?: TimelineResolver<(Maybe<TimelineResult>)[], TypeParent, TContext>;
|
||||
|
@ -6091,7 +6065,6 @@ export type IResolvers<TContext = SiemContext> = {
|
|||
SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers<TContext>;
|
||||
SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers<TContext>;
|
||||
KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers<TContext>;
|
||||
SortTimelineResult?: SortTimelineResultResolvers.Resolvers<TContext>;
|
||||
ResponseTimelines?: ResponseTimelinesResolvers.Resolvers<TContext>;
|
||||
Mutation?: MutationResolvers.Resolvers<TContext>;
|
||||
ResponseNote?: ResponseNoteResolvers.Resolvers<TContext>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -7,5 +7,37 @@
|
|||
export const toArray = <T = string>(value: T | T[] | null): T[] =>
|
||||
Array.isArray(value) ? value : value == null ? [] : [value];
|
||||
|
||||
export const toStringArray = <T = string>(value: T | T[] | null): T[] | string[] =>
|
||||
Array.isArray(value) ? value : value == null ? [] : [`${value}`];
|
||||
export const toStringArray = <T = string>(value: T | T[] | null): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce<string[]>((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}`];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 = <T>(
|
||||
fieldName: string,
|
||||
flattenedFields: T,
|
||||
hit: { _source: {} },
|
||||
hit: { fields: Record<string, unknown[]> },
|
||||
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 = <T>(
|
|||
...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 = <T>(
|
|||
...fieldName.split('.').reduceRight(
|
||||
// @ts-expect-error
|
||||
(obj, next) => ({ [next]: obj }),
|
||||
toStringArray<string>(get(esField, hit._source))
|
||||
toStringArray(hit.fields[fieldName])
|
||||
),
|
||||
}
|
||||
: get('node.ecs', flattenedFields),
|
||||
|
|
|
@ -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<TimelineEventsQu
|
|||
const totalCount = response.rawResponse.hits.total || 0;
|
||||
const hits = response.rawResponse.hits.hits;
|
||||
const edges: TimelineEdges[] = hits.map((hit) =>
|
||||
// @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))],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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<TimelineEventsDetailsItem[]>((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<string, unknown[]>): TimelineEventsDetailsItem[] =>
|
||||
Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((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,
|
||||
];
|
||||
}, []);
|
||||
|
|
|
@ -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<TimelineEven
|
|||
response: IEsSearchResponse<unknown>
|
||||
): Promise<TimelineEventsDetailsStrategyResponse> => {
|
||||
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,
|
||||
|
|
|
@ -21,6 +21,7 @@ export const buildTimelineDetailsQuery = (
|
|||
_id: [id],
|
||||
},
|
||||
},
|
||||
fields: ['*'],
|
||||
},
|
||||
size: 1,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue