—
@@ -292,11 +292,11 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
data-test-subj="stat-item"
>
—
@@ -616,11 +616,11 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
data-test-subj="stat-item"
>
1,714
@@ -857,10 +857,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
key="stat-items-field-uniqueDestinationIps"
>
2,359
@@ -957,10 +957,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
>
diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx
index 4887a5c32b59d..8453ec1cfb5d7 100644
--- a/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx
@@ -37,6 +37,14 @@ import { KpiNetworkData, KpiHostsData } from '../../graphql/types';
const from = new Date('2019-06-15T06:00:00.000Z').valueOf();
const to = new Date('2019-06-18T06:00:00.000Z').valueOf();
+jest.mock('../charts/areachart', () => {
+ return { AreaChart: () => };
+});
+
+jest.mock('../charts/barchart', () => {
+ return { BarChart: () => };
+});
+
describe('Stat Items Component', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
const state: State = mockGlobalState;
diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx b/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
index b52184f078c6f..110d146381709 100644
--- a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
@@ -27,7 +27,7 @@ import styled from 'styled-components';
import { KpiHostsData, KpiNetworkData } from '../../graphql/types';
import { AreaChart } from '../charts/areachart';
import { BarChart } from '../charts/barchart';
-import { ChartConfigsData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
+import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
import { getEmptyTagValue } from '../empty_value';
import { InspectButton } from '../inspect';
@@ -69,8 +69,8 @@ export interface StatItems {
}
export interface StatItemsProps extends StatItems {
- areaChart?: ChartConfigsData[];
- barChart?: ChartConfigsData[];
+ areaChart?: ChartSeriesData[];
+ barChart?: ChartSeriesData[];
from: number;
id: string;
narrowDateRange: UpdateDateRange;
@@ -79,6 +79,7 @@ export interface StatItemsProps extends StatItems {
export const numberFormatter = (value: string | number): string => value.toLocaleString();
const statItemBarchartRotation: Rotation = 90;
+const statItemChartCustomHeight = 74;
export const areachartConfigs = (config?: {
xTickFormatter: (value: number) => string;
@@ -95,6 +96,7 @@ export const areachartConfigs = (config?: {
settings: {
onBrushEnd: getOr(() => {}, 'onBrushEnd', config),
},
+ customHeight: statItemChartCustomHeight,
});
export const barchartConfigs = (config?: { onElementClick?: ElementClickListener }) => ({
@@ -109,6 +111,7 @@ export const barchartConfigs = (config?: { onElementClick?: ElementClickListener
onElementClick: getOr(() => {}, 'onElementClick', config),
rotation: statItemBarchartRotation,
},
+ customHeight: statItemChartCustomHeight,
});
export const addValueToFields = (
@@ -119,7 +122,7 @@ export const addValueToFields = (
export const addValueToAreaChart = (
fields: StatItem[],
data: KpiHostsData | KpiNetworkData
-): ChartConfigsData[] =>
+): ChartSeriesData[] =>
fields
.filter(field => get(`${field.key}Histogram`, data) != null)
.map(field => ({
@@ -131,9 +134,9 @@ export const addValueToAreaChart = (
export const addValueToBarChart = (
fields: StatItem[],
data: KpiHostsData | KpiNetworkData
-): ChartConfigsData[] => {
+): ChartSeriesData[] => {
if (fields.length === 0) return [];
- return fields.reduce((acc: ChartConfigsData[], field: StatItem, idx: number) => {
+ return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => {
const { key, color } = field;
const y: number | null = getOr(null, key, data);
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
diff --git a/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/events_over_time.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/events_over_time.gql_query.ts
new file mode 100644
index 0000000000000..aec0a32043040
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/events_over_time.gql_query.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 gql from 'graphql-tag';
+
+export const EventsOverTimeGqlQuery = gql`
+ query GetEventsOverTimeQuery(
+ $sourceId: ID!
+ $timerange: TimerangeInput!
+ $defaultIndex: [String!]!
+ $filterQuery: String
+ $inspect: Boolean!
+ ) {
+ source(id: $sourceId) {
+ id
+ EventsOverTime(
+ timerange: $timerange
+ filterQuery: $filterQuery
+ defaultIndex: $defaultIndex
+ ) {
+ eventsOverTime {
+ x
+ y
+ g
+ }
+ totalCount
+ inspect @include(if: $inspect) {
+ dsl
+ response
+ }
+ }
+ }
+ }
+`;
diff --git a/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/index.tsx
new file mode 100644
index 0000000000000..5ce4457792552
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/events/events_over_time/index.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { getOr } from 'lodash/fp';
+import React from 'react';
+import { Query } from 'react-apollo';
+import { connect } from 'react-redux';
+
+import chrome from 'ui/chrome';
+import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
+import { inputsModel, State, inputsSelectors, hostsModel } from '../../../store';
+import { createFilter, getDefaultFetchPolicy } from '../../helpers';
+import { QueryTemplate, QueryTemplateProps } from '../../query_template';
+
+import { EventsOverTimeGqlQuery } from './events_over_time.gql_query';
+import { GetEventsOverTimeQuery, MatrixOverTimeHistogramData } from '../../../graphql/types';
+
+const ID = 'eventsOverTimeQuery';
+
+export interface EventsArgs {
+ endDate: number;
+ eventsOverTime: MatrixOverTimeHistogramData[];
+ id: string;
+ inspect: inputsModel.InspectQuery;
+ loading: boolean;
+ refetch: inputsModel.Refetch;
+ startDate: number;
+ totalCount: number;
+}
+
+export interface OwnProps extends QueryTemplateProps {
+ children?: (args: EventsArgs) => React.ReactNode;
+ type: hostsModel.HostsType;
+}
+
+export interface EventsOverTimeComponentReduxProps {
+ isInspected: boolean;
+}
+
+type EventsOverTimeProps = OwnProps & EventsOverTimeComponentReduxProps;
+
+class EventsOverTimeComponentQuery extends QueryTemplate<
+ EventsOverTimeProps,
+ GetEventsOverTimeQuery.Query,
+ GetEventsOverTimeQuery.Variables
+> {
+ public render() {
+ const {
+ children,
+ filterQuery,
+ id = ID,
+ isInspected,
+ sourceId,
+ startDate,
+ endDate,
+ } = this.props;
+ return (
+
+ query={EventsOverTimeGqlQuery}
+ fetchPolicy={getDefaultFetchPolicy()}
+ notifyOnNetworkStatusChange
+ variables={{
+ filterQuery: createFilter(filterQuery),
+ sourceId,
+ timerange: {
+ interval: '12h',
+ from: startDate!,
+ to: endDate!,
+ },
+ defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
+ inspect: isInspected,
+ }}
+ >
+ {({ data, loading, refetch }) => {
+ const source = getOr({}, `source.EventsOverTime`, data);
+ const eventsOverTime = getOr([], `eventsOverTime`, source);
+ const totalCount = getOr(-1, 'totalCount', source);
+ return children!({
+ endDate: endDate!,
+ eventsOverTime,
+ id,
+ inspect: getOr(null, 'inspect', source),
+ loading,
+ refetch,
+ startDate: startDate!,
+ totalCount,
+ });
+ }}
+
+ );
+ }
+}
+
+const makeMapStateToProps = () => {
+ const getQuery = inputsSelectors.globalQueryByIdSelector();
+ const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => {
+ const { isInspected } = getQuery(state, id);
+ return {
+ isInspected,
+ };
+ };
+ return mapStateToProps;
+};
+
+export const EventsOverTimeQuery = connect(makeMapStateToProps)(EventsOverTimeComponentQuery);
diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
index 66f3975562774..404eb53f711d3 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json
+++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
@@ -916,6 +916,53 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "EventsOverTime",
+ "description": "",
+ "args": [
+ {
+ "name": "timerange",
+ "description": "",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filterQuery",
+ "description": "",
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "defaultValue": null
+ },
+ {
+ "name": "defaultIndex",
+ "description": "",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "OBJECT", "name": "EventsOverTimeData", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "Hosts",
"description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified",
@@ -5451,6 +5498,108 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "EventsOverTimeData",
+ "description": "",
+ "fields": [
+ {
+ "name": "inspect",
+ "description": "",
+ "args": [],
+ "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "eventsOverTime",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MatrixOverTimeHistogramData",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "totalCount",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MatrixOverTimeHistogramData",
+ "description": "",
+ "fields": [
+ {
+ "name": "x",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "y",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "g",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "INPUT_OBJECT",
"name": "HostsSortField",
diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts
index bdf18c3458a98..3e43c6d7db0ac 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts
@@ -117,6 +117,8 @@ export interface Source {
TimelineDetails: TimelineDetailsData;
LastEventTime: LastEventTimeData;
+
+ EventsOverTime: EventsOverTimeData;
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
Hosts: HostsData;
@@ -847,6 +849,22 @@ export interface LastEventTimeData {
inspect?: Inspect | null;
}
+export interface EventsOverTimeData {
+ inspect?: Inspect | null;
+
+ eventsOverTime: MatrixOverTimeHistogramData[];
+
+ totalCount: number;
+}
+
+export interface MatrixOverTimeHistogramData {
+ x: number;
+
+ y: number;
+
+ g: string;
+}
+
export interface HostsData {
edges: HostsEdges[];
@@ -1835,6 +1853,13 @@ export interface LastEventTimeSourceArgs {
defaultIndex: string[];
}
+export interface EventsOverTimeSourceArgs {
+ timerange: TimerangeInput;
+
+ filterQuery?: string | null;
+
+ defaultIndex: string[];
+}
export interface HostsSourceArgs {
id?: string | null;
@@ -2416,6 +2441,58 @@ export namespace GetDomainsQuery {
};
}
+export namespace GetEventsOverTimeQuery {
+ export type Variables = {
+ sourceId: string;
+ timerange: TimerangeInput;
+ defaultIndex: string[];
+ filterQuery?: string | null;
+ inspect: boolean;
+ };
+
+ export type Query = {
+ __typename?: 'Query';
+
+ source: Source;
+ };
+
+ export type Source = {
+ __typename?: 'Source';
+
+ id: string;
+
+ EventsOverTime: EventsOverTime;
+ };
+
+ export type EventsOverTime = {
+ __typename?: 'EventsOverTimeData';
+
+ eventsOverTime: _EventsOverTime[];
+
+ totalCount: number;
+
+ inspect?: Inspect | null;
+ };
+
+ export type _EventsOverTime = {
+ __typename?: 'MatrixOverTimeHistogramData';
+
+ x: number;
+
+ y: number;
+
+ g: string;
+ };
+
+ export type Inspect = {
+ __typename?: 'Inspect';
+
+ dsl: string[];
+
+ response: string[];
+ };
+}
+
export namespace GetLastEventTimeQuery {
export type Variables = {
sourceId: string;
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx
index cc0c9253623ba..a115224dd24db 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx
@@ -55,6 +55,9 @@ const HostDetailsBodyComponent = React.memo(
to: fromTo.to,
});
},
+ updateDateRange: (min: number, max: number) => {
+ setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
+ },
})}
>
) : null
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx
index 0b8087aff7f88..192b692253316 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx
@@ -136,7 +136,6 @@ const HostDetailsComponent = React.memo(
)}
-
(
to: fromTo.to,
});
},
+ updateDateRange: (min: number, max: number) => {
+ setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
+ },
})}
>
) : null
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx
new file mode 100644
index 0000000000000..37283b7d4aa8e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx
@@ -0,0 +1,387 @@
+/*
+ * 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 { StaticIndexPattern } from 'ui/index_patterns';
+import { getOr, omit } from 'lodash/fp';
+import React from 'react';
+import { EuiSpacer } from '@elastic/eui';
+import * as i18n from './translations';
+
+import { HostsTable, UncommonProcessTable } from '../../components/page/hosts';
+
+import { HostsQuery } from '../../containers/hosts';
+import { AuthenticationTable } from '../../components/page/hosts/authentications_table';
+import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table';
+import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
+import { InspectQuery, Refetch } from '../../store/inputs/model';
+import { NarrowDateRange } from '../../components/ml/types';
+import { hostsModel } from '../../store';
+import { manageQuery } from '../../components/page/manage_query';
+import { AuthenticationsQuery } from '../../containers/authentications';
+import { ESTermQuery } from '../../../common/typed_json';
+import { HostsTableType } from '../../store/hosts/model';
+import { StatefulEventsViewer } from '../../components/events_viewer';
+import { NavTab } from '../../components/navigation/types';
+import { EventsOverTimeQuery } from '../../containers/events/events_over_time';
+import { EventsOverTimeHistogram } from '../../components/page/hosts/events_over_time';
+import { UpdateDateRange } from '../../components/charts/common';
+
+const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/hosts/${tabName}`;
+const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => {
+ return `#/hosts/${hostName}/${tabName}`;
+};
+
+type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts &
+ HostsTableType.authentications &
+ HostsTableType.uncommonProcesses &
+ HostsTableType.events;
+
+type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies;
+
+export type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission;
+
+type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications &
+ HostsTableType.uncommonProcesses &
+ HostsTableType.events;
+
+type KeyHostDetailsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission &
+ HostsTableType.anomalies;
+
+export type KeyHostDetailsNavTab =
+ | KeyHostDetailsNavTabWithoutMlPermission
+ | KeyHostDetailsNavTabWithMlPermission;
+
+export type HostsNavTab = Record;
+
+export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => {
+ const hostsNavTabs = {
+ [HostsTableType.hosts]: {
+ id: HostsTableType.hosts,
+ name: i18n.NAVIGATION_ALL_HOSTS_TITLE,
+ href: getTabsOnHostsUrl(HostsTableType.hosts),
+ disabled: false,
+ urlKey: 'host',
+ },
+ [HostsTableType.authentications]: {
+ id: HostsTableType.authentications,
+ name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE,
+ href: getTabsOnHostsUrl(HostsTableType.authentications),
+ disabled: false,
+ urlKey: 'host',
+ },
+ [HostsTableType.uncommonProcesses]: {
+ id: HostsTableType.uncommonProcesses,
+ name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE,
+ href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses),
+ disabled: false,
+ urlKey: 'host',
+ },
+ [HostsTableType.anomalies]: {
+ id: HostsTableType.anomalies,
+ name: i18n.NAVIGATION_ANOMALIES_TITLE,
+ href: getTabsOnHostsUrl(HostsTableType.anomalies),
+ disabled: false,
+ urlKey: 'host',
+ },
+ [HostsTableType.events]: {
+ id: HostsTableType.events,
+ name: i18n.NAVIGATION_EVENTS_TITLE,
+ href: getTabsOnHostsUrl(HostsTableType.events),
+ disabled: false,
+ urlKey: 'host',
+ },
+ };
+
+ return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs);
+};
+
+export const navTabsHostDetails = (
+ hostName: string,
+ hasMlUserPermissions: boolean
+): Record => {
+ const hostDetailsNavTabs = {
+ [HostsTableType.authentications]: {
+ id: HostsTableType.authentications,
+ name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE,
+ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications),
+ disabled: false,
+ urlKey: 'host',
+ isDetailPage: true,
+ },
+ [HostsTableType.uncommonProcesses]: {
+ id: HostsTableType.uncommonProcesses,
+ name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE,
+ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses),
+ disabled: false,
+ urlKey: 'host',
+ isDetailPage: true,
+ },
+ [HostsTableType.anomalies]: {
+ id: HostsTableType.anomalies,
+ name: i18n.NAVIGATION_ANOMALIES_TITLE,
+ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies),
+ disabled: false,
+ urlKey: 'host',
+ isDetailPage: true,
+ },
+ [HostsTableType.events]: {
+ id: HostsTableType.events,
+ name: i18n.NAVIGATION_EVENTS_TITLE,
+ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events),
+ disabled: false,
+ urlKey: 'host',
+ isDetailPage: true,
+ },
+ };
+
+ return hasMlUserPermissions
+ ? hostDetailsNavTabs
+ : omit(HostsTableType.anomalies, hostDetailsNavTabs);
+};
+
+interface OwnProps {
+ type: hostsModel.HostsType;
+ startDate: number;
+ endDate: number;
+ filterQuery?: string | ESTermQuery;
+ kqlQueryExpression: string;
+}
+export type HostsComponentsQueryProps = OwnProps & {
+ deleteQuery?: ({ id }: { id: string }) => void;
+ indexPattern: StaticIndexPattern;
+ skip: boolean;
+ setQuery: ({
+ id,
+ inspect,
+ loading,
+ refetch,
+ }: {
+ id: string;
+ inspect: InspectQuery | null;
+ loading: boolean;
+ refetch: Refetch;
+ }) => void;
+ updateDateRange?: UpdateDateRange;
+ filterQueryExpression?: string;
+ hostName?: string;
+};
+
+export type AnomaliesQueryTabBodyProps = OwnProps & {
+ skip: boolean;
+ narrowDateRange: NarrowDateRange;
+ hostName?: string;
+};
+
+const AuthenticationTableManage = manageQuery(AuthenticationTable);
+const HostsTableManage = manageQuery(HostsTable);
+const UncommonProcessTableManage = manageQuery(UncommonProcessTable);
+
+export const HostsQueryTabBody = ({
+ deleteQuery,
+ endDate,
+ filterQuery,
+ indexPattern,
+ skip,
+ setQuery,
+ startDate,
+ type,
+}: HostsComponentsQueryProps) => {
+ return (
+
+ {({ hosts, totalCount, loading, pageInfo, loadPage, id, inspect, isInspected, refetch }) => (
+
+ )}
+
+ );
+};
+
+export const AuthenticationsQueryTabBody = ({
+ deleteQuery,
+ endDate,
+ filterQuery,
+ skip,
+ setQuery,
+ startDate,
+ type,
+}: HostsComponentsQueryProps) => {
+ return (
+
+ {({
+ authentications,
+ totalCount,
+ loading,
+ pageInfo,
+ loadPage,
+ id,
+ inspect,
+ isInspected,
+ refetch,
+ }) => {
+ return (
+
+ );
+ }}
+
+ );
+};
+
+export const UncommonProcessTabBody = ({
+ deleteQuery,
+ endDate,
+ filterQuery,
+ skip,
+ setQuery,
+ startDate,
+ type,
+}: HostsComponentsQueryProps) => {
+ return (
+
+ {({
+ uncommonProcesses,
+ totalCount,
+ loading,
+ pageInfo,
+ loadPage,
+ id,
+ inspect,
+ isInspected,
+ refetch,
+ }) => (
+
+ )}
+
+ );
+};
+
+export const AnomaliesTabBody = ({
+ endDate,
+ skip,
+ startDate,
+ type,
+ narrowDateRange,
+ hostName,
+}: AnomaliesQueryTabBodyProps) => {
+ return (
+
+ );
+};
+const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram);
+
+export const EventsTabBody = ({
+ endDate,
+ kqlQueryExpression,
+ startDate,
+ setQuery,
+ filterQuery,
+ updateDateRange = () => {},
+}: HostsComponentsQueryProps) => {
+ const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
+
+ return (
+ <>
+
+ {({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => (
+
+ )}
+
+
+
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx
index 928886cae3ad3..c7b7f1c0eb643 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx
@@ -5,22 +5,58 @@
*/
import React from 'react';
+import { EuiSpacer } from '@elastic/eui';
import { StatefulEventsViewer } from '../../../components/events_viewer';
import { HostsComponentsQueryProps } from './types';
+import { manageQuery } from '../../../components/page/manage_query';
+import { EventsOverTimeHistogram } from '../../../components/page/hosts/events_over_time';
+import { EventsOverTimeQuery } from '../../../containers/events/events_over_time';
+import { hostsModel } from '../../../store/hosts';
const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
+const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram);
export const EventsQueryTabBody = ({
endDate,
kqlQueryExpression,
startDate,
-}: HostsComponentsQueryProps) => (
-
-);
+ setQuery,
+ filterQuery,
+ updateDateRange = () => {},
+}: HostsComponentsQueryProps) => {
+ return (
+ <>
+
+ {({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => (
+
+ )}
+
+
+
+ >
+ );
+};
EventsQueryTabBody.displayName = 'EventsQueryTabBody';
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts
index 7161772aac495..4554fa1351f5f 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts
@@ -12,6 +12,7 @@ import { InspectQuery, Refetch } from '../../../store/inputs/model';
import { HostsTableType } from '../../../store/hosts/model';
import { NavTab } from '../../../components/navigation/types';
+import { UpdateDateRange } from '../../../components/charts/common';
export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts &
HostsTableType.authentications &
@@ -53,6 +54,7 @@ export type HostsComponentsQueryProps = QueryTabBodyProps & {
loading: boolean;
refetch: Refetch;
}) => void;
+ updateDateRange?: UpdateDateRange;
narrowDateRange?: NarrowDateRange;
};
diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts
index 2e71399973e9f..09494594c7286 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts
@@ -32,6 +32,11 @@ export interface EventsResolversDeps {
events: Events;
}
+type QueryEventsOverTimeResolver = ChildResolverOf<
+ AppResolverOf,
+ QuerySourceResolver
+>;
+
export const createEventsResolvers = (
libs: EventsResolversDeps
): {
@@ -39,6 +44,7 @@ export const createEventsResolvers = (
Timeline: QueryTimelineResolver;
TimelineDetails: QueryTimelineDetailsResolver;
LastEventTime: QueryLastEventTimeResolver;
+ EventsOverTime: QueryEventsOverTimeResolver;
};
} => ({
Source: {
@@ -65,6 +71,13 @@ export const createEventsResolvers = (
};
return libs.events.getLastEventTimeData(req, options);
},
+ async EventsOverTime(source, args, { req }, info) {
+ const options = {
+ ...createOptions(source, args, info),
+ defaultIndex: args.defaultIndex,
+ };
+ return libs.events.getEventsOverTime(req, options);
+ },
},
});
diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts
index 3b71977bc0d47..073fd60dbf1ed 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts
@@ -68,6 +68,18 @@ export const eventsSchema = gql`
network
}
+ type MatrixOverTimeHistogramData {
+ x: Float!
+ y: Float!
+ g: String!
+ }
+
+ type EventsOverTimeData {
+ inspect: Inspect
+ eventsOverTime: [MatrixOverTimeHistogramData!]!
+ totalCount: Float!
+ }
+
extend type Source {
Timeline(
pagination: PaginationInput!
@@ -88,5 +100,10 @@ export const eventsSchema = gql`
details: LastTimeDetails!
defaultIndex: [String!]!
): LastEventTimeData!
+ EventsOverTime(
+ timerange: TimerangeInput!
+ filterQuery: String
+ defaultIndex: [String!]!
+ ): EventsOverTimeData!
}
`;
diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts
index 87e6b3719a4bb..8fd80be5d04d0 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts
@@ -146,6 +146,8 @@ export interface Source {
TimelineDetails: TimelineDetailsData;
LastEventTime: LastEventTimeData;
+
+ EventsOverTime: EventsOverTimeData;
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
Hosts: HostsData;
@@ -876,6 +878,22 @@ export interface LastEventTimeData {
inspect?: Inspect | null;
}
+export interface EventsOverTimeData {
+ inspect?: Inspect | null;
+
+ eventsOverTime: MatrixOverTimeHistogramData[];
+
+ totalCount: number;
+}
+
+export interface MatrixOverTimeHistogramData {
+ x: number;
+
+ y: number;
+
+ g: string;
+}
+
export interface HostsData {
edges: HostsEdges[];
@@ -1864,6 +1882,13 @@ export interface LastEventTimeSourceArgs {
defaultIndex: string[];
}
+export interface EventsOverTimeSourceArgs {
+ timerange: TimerangeInput;
+
+ filterQuery?: string | null;
+
+ defaultIndex: string[];
+}
export interface HostsSourceArgs {
id?: string | null;
@@ -2497,6 +2522,8 @@ export namespace SourceResolvers {
TimelineDetails?: TimelineDetailsResolver;
LastEventTime?: LastEventTimeResolver;
+
+ EventsOverTime?: EventsOverTimeResolver;
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
Hosts?: HostsResolver;
@@ -2609,6 +2636,19 @@ export namespace SourceResolvers {
defaultIndex: string[];
}
+ export type EventsOverTimeResolver<
+ R = EventsOverTimeData,
+ Parent = Source,
+ Context = SiemContext
+ > = Resolver;
+ export interface EventsOverTimeArgs {
+ timerange: TimerangeInput;
+
+ filterQuery?: string | null;
+
+ defaultIndex: string[];
+ }
+
export type HostsResolver = Resolver<
R,
Parent,
@@ -5204,6 +5244,58 @@ export namespace LastEventTimeDataResolvers {
> = Resolver;
}
+export namespace EventsOverTimeDataResolvers {
+ export interface Resolvers {
+ inspect?: InspectResolver;
+
+ eventsOverTime?: EventsOverTimeResolver;
+
+ totalCount?: TotalCountResolver;
+ }
+
+ export type InspectResolver<
+ R = Inspect | null,
+ Parent = EventsOverTimeData,
+ Context = SiemContext
+ > = Resolver;
+ export type EventsOverTimeResolver<
+ R = MatrixOverTimeHistogramData[],
+ Parent = EventsOverTimeData,
+ Context = SiemContext
+ > = Resolver;
+ export type TotalCountResolver<
+ R = number,
+ Parent = EventsOverTimeData,
+ Context = SiemContext
+ > = Resolver;
+}
+
+export namespace MatrixOverTimeHistogramDataResolvers {
+ export interface Resolvers {
+ x?: XResolver;
+
+ y?: YResolver;
+
+ g?: GResolver;
+ }
+
+ export type XResolver<
+ R = number,
+ Parent = MatrixOverTimeHistogramData,
+ Context = SiemContext
+ > = Resolver;
+ export type YResolver<
+ R = number,
+ Parent = MatrixOverTimeHistogramData,
+ Context = SiemContext
+ > = Resolver;
+ export type GResolver<
+ R = string,
+ Parent = MatrixOverTimeHistogramData,
+ Context = SiemContext
+ > = Resolver;
+}
+
export namespace HostsDataResolvers {
export interface Resolvers {
edges?: EdgesResolver;
diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts
index 543fe0580b8a4..6dbb75d28149b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts
@@ -25,12 +25,13 @@ import {
TimelineData,
TimelineDetailsData,
TimelineEdges,
+ EventsOverTimeData,
} from '../../graphql/types';
import { baseCategoryFields } from '../../utils/beat_schema/8.0.0';
import { reduceFields } from '../../utils/build_query/reduce_fields';
import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query';
import { eventFieldsMap } from '../ecs_fields';
-import { FrameworkAdapter, FrameworkRequest } from '../framework';
+import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework';
import { TermAggregation } from '../types';
import { buildDetailsQuery, buildTimelineQuery } from './query.dsl';
@@ -42,7 +43,10 @@ import {
LastEventTimeRequestOptions,
RequestDetailsOptions,
TimelineRequestOptions,
+ EventsActionGroupData,
} from './types';
+import { buildEventsOverTimeQuery } from './query.events_over_time.dsl';
+import { MatrixOverTimeHistogramData } from '../../../public/graphql/types';
export class ElasticsearchEventsAdapter implements EventsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
@@ -125,8 +129,65 @@ export class ElasticsearchEventsAdapter implements EventsAdapter {
lastSeen: getOr(null, 'aggregations.last_seen_event.value_as_string', response),
};
}
+
+ public async getEventsOverTime(
+ request: FrameworkRequest,
+ options: RequestBasicOptions
+ ): Promise {
+ const dsl = buildEventsOverTimeQuery(options);
+ const response = await this.framework.callWithRequest(
+ request,
+ 'search',
+ dsl
+ );
+ const totalCount = getOr(0, 'hits.total.value', response);
+ const eventsOverTimeBucket = getOr([], 'aggregations.eventActionGroup.buckets', response);
+ const inspect = {
+ dsl: [inspectStringifyObject(dsl)],
+ response: [inspectStringifyObject(response)],
+ };
+ return {
+ inspect,
+ eventsOverTime: getEventsOverTimeByActionName(eventsOverTimeBucket),
+ totalCount,
+ };
+ }
}
+/**
+ * Not in use at the moment,
+ * reserved this parser for next feature of switchign between total events and grouped events
+ */
+export const getTotalEventsOverTime = (
+ data: EventsActionGroupData[]
+): MatrixOverTimeHistogramData[] => {
+ return data && data.length > 0
+ ? data.map(({ key, doc_count }) => ({
+ x: key,
+ y: doc_count,
+ g: 'total events',
+ }))
+ : [];
+};
+
+const getEventsOverTimeByActionName = (
+ data: EventsActionGroupData[]
+): MatrixOverTimeHistogramData[] => {
+ let result: MatrixOverTimeHistogramData[] = [];
+ data.forEach(({ key: group, events }) => {
+ const eventsData = getOr([], 'buckets', events).map(
+ ({ key, doc_count }: { key: number; doc_count: number }) => ({
+ x: key,
+ y: doc_count,
+ g: group,
+ })
+ );
+ result = [...result, ...eventsData];
+ });
+
+ return result;
+};
+
export const formatEventsData = (
fields: readonly string[],
hit: EventHit,
diff --git a/x-pack/legacy/plugins/siem/server/lib/events/index.ts b/x-pack/legacy/plugins/siem/server/lib/events/index.ts
index 9c1f87aa3d8bf..9e2457904f8c0 100644
--- a/x-pack/legacy/plugins/siem/server/lib/events/index.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/events/index.ts
@@ -5,7 +5,7 @@
*/
import { LastEventTimeData, TimelineData, TimelineDetailsData } from '../../graphql/types';
-import { FrameworkRequest } from '../framework';
+import { FrameworkRequest, RequestBasicOptions } from '../framework';
export * from './elasticsearch_adapter';
import {
EventsAdapter,
@@ -13,6 +13,7 @@ import {
LastEventTimeRequestOptions,
RequestDetailsOptions,
} from './types';
+import { EventsOverTimeData } from '../../../public/graphql/types';
export class Events {
constructor(private readonly adapter: EventsAdapter) {}
@@ -37,4 +38,11 @@ export class Events {
): Promise {
return this.adapter.getLastEventTimeData(req, options);
}
+
+ public async getEventsOverTime(
+ req: FrameworkRequest,
+ options: RequestBasicOptions
+ ): Promise {
+ return this.adapter.getEventsOverTime(req, options);
+ }
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts
new file mode 100644
index 0000000000000..e655485638e16
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { createQueryFilterClauses, calculateTimeseriesInterval } from '../../utils/build_query';
+import { RequestBasicOptions } from '../framework';
+
+export const buildEventsOverTimeQuery = ({
+ filterQuery,
+ timerange: { from, to },
+ defaultIndex,
+ sourceConfiguration: {
+ fields: { timestamp },
+ },
+}: RequestBasicOptions) => {
+ const filter = [
+ ...createQueryFilterClauses(filterQuery),
+ {
+ range: {
+ [timestamp]: {
+ gte: from,
+ lte: to,
+ },
+ },
+ },
+ ];
+
+ const getHistogramAggregation = () => {
+ const minIntervalSeconds = 10;
+ const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds);
+ const histogramTimestampField = '@timestamp';
+ const dateHistogram = {
+ date_histogram: {
+ field: histogramTimestampField,
+ fixed_interval: `${interval}s`,
+ },
+ };
+ const autoDateHistogram = {
+ auto_date_histogram: {
+ field: histogramTimestampField,
+ buckets: 36,
+ },
+ };
+ return {
+ eventActionGroup: {
+ terms: {
+ field: 'event.action',
+ missing: 'All others',
+ order: {
+ _count: 'desc',
+ },
+ size: 10,
+ },
+ aggs: {
+ events: interval ? dateHistogram : autoDateHistogram,
+ },
+ },
+ };
+ };
+
+ const dslQuery = {
+ index: defaultIndex,
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ body: {
+ aggregations: getHistogramAggregation(),
+ query: {
+ bool: {
+ filter,
+ },
+ },
+ size: 0,
+ track_total_hits: true,
+ },
+ };
+
+ return dslQuery;
+};
diff --git a/x-pack/legacy/plugins/siem/server/lib/events/types.ts b/x-pack/legacy/plugins/siem/server/lib/events/types.ts
index 30e49b8a37cc7..2da0ff13638e1 100644
--- a/x-pack/legacy/plugins/siem/server/lib/events/types.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/events/types.ts
@@ -11,8 +11,14 @@ import {
SourceConfiguration,
TimelineData,
TimelineDetailsData,
+ EventsOverTimeData,
} from '../../graphql/types';
-import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework';
+import {
+ FrameworkRequest,
+ RequestOptions,
+ RequestOptionsPaginated,
+ RequestBasicOptions,
+} from '../framework';
import { SearchHit } from '../types';
export interface EventsAdapter {
@@ -25,6 +31,10 @@ export interface EventsAdapter {
req: FrameworkRequest,
options: LastEventTimeRequestOptions
): Promise;
+ getEventsOverTime(
+ req: FrameworkRequest,
+ options: RequestBasicOptions
+ ): Promise;
}
export interface TimelineRequestOptions extends RequestOptions {
@@ -77,3 +87,17 @@ export interface RequestDetailsOptions {
eventId: string;
defaultIndex: string[];
}
+
+interface EventsOverTimeHistogramData {
+ key_as_string: string;
+ key: number;
+ doc_count: number;
+}
+
+export interface EventsActionGroupData {
+ key: number;
+ events: {
+ bucket: EventsOverTimeHistogramData[];
+ };
+ doc_count: number;
+}
diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts
new file mode 100644
index 0000000000000..3eaaa6c30a4fa
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+/*
+ ** Applying the same logic as:
+ ** x-pack/legacy/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js
+ */
+import moment from 'moment';
+import { get } from 'lodash/fp';
+const d = moment.duration;
+
+const roundingRules = [
+ [d(500, 'ms'), d(100, 'ms')],
+ [d(5, 'second'), d(1, 'second')],
+ [d(7.5, 'second'), d(5, 'second')],
+ [d(15, 'second'), d(10, 'second')],
+ [d(45, 'second'), d(30, 'second')],
+ [d(3, 'minute'), d(1, 'minute')],
+ [d(9, 'minute'), d(5, 'minute')],
+ [d(20, 'minute'), d(10, 'minute')],
+ [d(45, 'minute'), d(30, 'minute')],
+ [d(2, 'hour'), d(1, 'hour')],
+ [d(6, 'hour'), d(3, 'hour')],
+ [d(24, 'hour'), d(12, 'hour')],
+ [d(1, 'week'), d(1, 'd')],
+ [d(3, 'week'), d(1, 'week')],
+ [d(1, 'year'), d(1, 'month')],
+ [Infinity, d(1, 'year')],
+];
+
+const revRoundingRules = roundingRules.slice(0).reverse();
+
+const find = (
+ rules: Array>,
+ check: (
+ bound: number | moment.Duration,
+ interval: number | moment.Duration,
+ target: number
+ ) => number | moment.Duration | undefined,
+ last?: boolean
+): ((buckets: number, duration: number | moment.Duration) => moment.Duration | undefined) => {
+ const pick = (buckets: number, duration: number | moment.Duration): number | moment.Duration => {
+ const target =
+ typeof duration === 'number' ? duration / buckets : duration.asMilliseconds() / buckets;
+ let lastResp = null;
+
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i];
+ const resp = check(rule[0], rule[1], target);
+
+ if (resp == null) {
+ if (last) {
+ if (lastResp) return lastResp;
+ break;
+ }
+ }
+
+ if (!last && resp) return resp;
+ lastResp = resp;
+ }
+
+ // fallback to just a number of milliseconds, ensure ms is >= 1
+ const ms = Math.max(Math.floor(target), 1);
+ return moment.duration(ms, 'ms');
+ };
+
+ return (buckets, duration) => {
+ const interval = pick(buckets, duration);
+ const intervalData = get('_data', interval);
+ if (intervalData) return moment.duration(intervalData);
+ };
+};
+
+export const calculateAuto = {
+ near: find(
+ revRoundingRules,
+ (bound, interval, target) => {
+ if (bound > target) return interval;
+ },
+ true
+ ),
+ lessThan: find(revRoundingRules, (_bound, interval, target) => {
+ if (interval < target) return interval;
+ }),
+ atLeast: find(revRoundingRules, (_bound, interval, target) => {
+ if (interval <= target) return interval;
+ }),
+};
+
+export const calculateTimeseriesInterval = (
+ lowerBoundInMsSinceEpoch: number,
+ upperBoundInMsSinceEpoch: number,
+ minIntervalSeconds: number
+) => {
+ const duration = moment.duration(upperBoundInMsSinceEpoch - lowerBoundInMsSinceEpoch, 'ms');
+
+ const matchedInterval = calculateAuto.near(50, duration);
+
+ return matchedInterval ? Math.max(matchedInterval.asSeconds(), 1) : null;
+};
diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/index.ts b/x-pack/legacy/plugins/siem/server/utils/build_query/index.ts
index be247d28aaaf8..c97e78aad2b69 100644
--- a/x-pack/legacy/plugins/siem/server/utils/build_query/index.ts
+++ b/x-pack/legacy/plugins/siem/server/utils/build_query/index.ts
@@ -7,6 +7,7 @@
export * from './fields';
export * from './filters';
export * from './merge_fields_with_hits';
+export * from './calculate_timeseries_interval';
export const assertUnreachable = (
x: never,
diff --git a/x-pack/test/api_integration/apis/siem/events_over_time.ts b/x-pack/test/api_integration/apis/siem/events_over_time.ts
new file mode 100644
index 0000000000000..10b81734b7b79
--- /dev/null
+++ b/x-pack/test/api_integration/apis/siem/events_over_time.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { EventsOverTimeGqlQuery } from '../../../../legacy/plugins/siem/public/containers/events/events_over_time/events_over_time.gql_query';
+import { GetEventsOverTimeQuery } from '../../../../legacy/plugins/siem/public/graphql/types';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const client = getService('siemGraphQLClient');
+ const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf();
+ const TO = new Date('3000-01-01T00:00:00.000Z').valueOf();
+ describe('Events over time', () => {
+ describe('With filebeat', () => {
+ before(() => esArchiver.load('filebeat/default'));
+ after(() => esArchiver.unload('filebeat/default'));
+
+ it('Make sure that we get events over time data', () => {
+ return client
+ .query({
+ query: EventsOverTimeGqlQuery,
+ variables: {
+ sourceId: 'default',
+ timerange: {
+ interval: '12h',
+ to: TO,
+ from: FROM,
+ },
+ defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
+ inspect: false,
+ },
+ })
+ .then(resp => {
+ const expectedData = [
+ {
+ x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
+ y: 4884,
+ g: 'All others',
+ __typename: 'MatrixOverTimeHistogramData',
+ },
+ {
+ x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
+ y: 1273,
+ g: 'netflow_flow',
+ __typename: 'MatrixOverTimeHistogramData',
+ },
+ ];
+ const eventsOverTime = resp.data.source.EventsOverTime;
+ expect(eventsOverTime.eventsOverTime).to.eql(expectedData);
+ });
+ });
+ });
+
+ describe('With packetbeat', () => {
+ before(() => esArchiver.load('packetbeat/default'));
+ after(() => esArchiver.unload('packetbeat/default'));
+
+ it('Make sure that we get events over time data', () => {
+ return client
+ .query({
+ query: EventsOverTimeGqlQuery,
+ variables: {
+ sourceId: 'default',
+ timerange: {
+ interval: '12h',
+ to: TO,
+ from: FROM,
+ },
+ defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
+ inspect: false,
+ },
+ })
+ .then(resp => {
+ const expectedData = [
+ {
+ x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
+ y: 4884,
+ g: 'All others',
+ __typename: 'MatrixOverTimeHistogramData',
+ },
+ {
+ x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
+ y: 1273,
+ g: 'netflow_flow',
+ __typename: 'MatrixOverTimeHistogramData',
+ },
+ ];
+ const eventsOverTime = resp.data.source.EventsOverTime;
+ expect(eventsOverTime.eventsOverTime).to.eql(expectedData);
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/apis/siem/index.js b/x-pack/test/api_integration/apis/siem/index.js
index 8213fcb85a297..a28c2f42a52df 100644
--- a/x-pack/test/api_integration/apis/siem/index.js
+++ b/x-pack/test/api_integration/apis/siem/index.js
@@ -8,6 +8,7 @@ export default function ({ loadTestFile }) {
describe('Siem GraphQL Endpoints', () => {
loadTestFile(require.resolve('./authentications'));
loadTestFile(require.resolve('./domains'));
+ loadTestFile(require.resolve('./events_over_time'));
loadTestFile(require.resolve('./hosts'));
loadTestFile(require.resolve('./kpi_network'));
loadTestFile(require.resolve('./kpi_hosts'));