diff --git a/x-pack/plugins/siem/public/components/card_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/card_items/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 4fb9a0719381f..0000000000000 --- a/x-pack/plugins/siem/public/components/card_items/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Card Items rendering it renders loading icons 1`] = ` - -`; - -exports[`Card Items rendering it renders the default widget 1`] = ` - -`; diff --git a/x-pack/plugins/siem/public/components/card_items/index.test.tsx b/x-pack/plugins/siem/public/components/card_items/index.test.tsx deleted file mode 100644 index f7b6e2b5efb06..0000000000000 --- a/x-pack/plugins/siem/public/components/card_items/index.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - // @ts-ignore - EuiCard, -} from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; - -import { CardItemsComponent, CardItemsProps } from '.'; - -describe('Card Items', () => { - describe('rendering', () => { - test('it renders loading icons', () => { - const mockCardItemsData: CardItemsProps = { - fields: [ - { - key: 'networkEvents', - description: 'NETWORK_EVENTS', - value: null, - }, - ], - isLoading: true, - key: 'mock-key', - }; - const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - test('it renders the default widget', () => { - const mockCardItemsData: CardItemsProps = { - fields: [ - { - key: 'networkEvents', - description: 'NETWORK_EVENTS', - value: null, - }, - ], - isLoading: false, - key: 'mock-key', - }; - const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - it('should handle multiple titles', () => { - const mockCardItemsData: CardItemsProps = { - fields: [ - { - key: 'uniqueSourcePrivateIps', - description: 'UNIQUE_SOURCE_PRIVATE_IPS', - value: null, - }, - { - key: 'uniqueDestinationPrivateIps', - description: 'UNIQUE_DESTINATION_PRIVATE_IPS', - value: null, - }, - ], - description: 'UNIQUE_PRIVATE_IPS', - isLoading: false, - key: 'mock-keys', - }; - const wrapper = mount(); - expect(wrapper.find(EuiCard).prop('title')).toHaveLength(2); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/card_items/index.tsx b/x-pack/plugins/siem/public/components/card_items/index.tsx deleted file mode 100644 index 5aea826a70972..0000000000000 --- a/x-pack/plugins/siem/public/components/card_items/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - // @ts-ignore - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, -} from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React from 'react'; -import { pure } from 'recompose'; - -import { getEmptyTagValue } from '../empty_value'; - -export interface CardItem { - key: string; - description: string; - value: number | undefined | null; -} - -export interface CardItems { - fields: CardItem[]; - description?: string; -} - -export interface CardItemsProps extends CardItems { - isLoading: boolean; - key: string; -} - -const CardTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( - ({ isLoading, value }) => ( - <> - {isLoading ? ( - - ) : value != null ? ( - numeral(value).format('0,0') - ) : ( - getEmptyTagValue() - )} - > - ) -); - -export const CardItemsComponent = pure( - ({ fields, description, isLoading, key }) => ( - - {fields.length === 1 ? ( - } - description={fields[0].description} - /> - ) : ( - ( - - - - - - {field.description} - - - ))} - description={description} - /> - )} - - ) -); diff --git a/x-pack/plugins/siem/public/components/page/hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/index.tsx index 40bc53cff1b65..e4962ea1806ab 100644 --- a/x-pack/plugins/siem/public/components/page/hosts/index.tsx +++ b/x-pack/plugins/siem/public/components/page/hosts/index.tsx @@ -8,3 +8,4 @@ export * from './authentications_table'; export * from './events_table'; export * from './hosts_table'; export * from './uncommon_process_table'; +export * from './kpi_hosts'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx new file mode 100644 index 0000000000000..014d7d5b283b2 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import { get, getOr } from 'lodash/fp'; +import React from 'react'; +import { pure } from 'recompose'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { KpiHostsData } from '../../../../graphql/types'; +import { + AreaChartData, + BarChartData, + StatItem, + StatItems, + StatItemsComponent, + StatItemsProps, +} from '../../../stat_items'; +import * as i18n from './translations'; + +interface KpiHostsProps { + data: KpiHostsData; + loading: boolean; +} + +const euiColorVis0 = '#00B3A4'; +const euiColorVis1 = '#3185FC'; +const euiColorVis2 = '#DB1374'; +const euiColorVis3 = '#490092'; +const euiColorVis9 = '#920000'; + +const fieldTitleMapping: StatItems[] = [ + { + fields: [ + { + key: 'hosts', + value: null, + color: euiColorVis1, + icon: 'storage', + }, + ], + enableAreaChart: true, + grow: 2, + description: i18n.HOSTS, + }, + { + fields: [ + { + key: 'authSuccess', + description: i18n.AUTHENTICATION_SUCCESS, + value: null, + color: euiColorVis0, + icon: 'check', + }, + { + key: 'authFailure', + description: i18n.AUTHENTICATION_FAILURE, + value: null, + color: euiColorVis9, + icon: 'cross', + }, + ], + enableAreaChart: true, + enableBarChart: true, + grow: 4, + description: i18n.AUTHENTICATION, + }, + { + fields: [ + { + key: 'uniqueSourceIps', + name: i18n.UNIQUE_SOURCE_IPS_ABBREVIATION, + description: i18n.UNIQUE_SOURCE_IPS, + value: null, + color: euiColorVis2, + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationIps', + description: i18n.UNIQUE_DESTINATION_IPS, + value: null, + color: euiColorVis3, + icon: 'visMapCoordinate', + }, + ], + enableAreaChart: true, + enableBarChart: true, + grow: 4, + description: i18n.UNIQUE_IPS, + }, +]; + +export const KpiHostsComponent = pure(({ data, loading }) => { + return loading ? ( + + + + + + ) : ( + + {fieldTitleMapping.map(stat => { + let statItemProps: StatItemsProps = { + ...stat, + key: `kpi-hosts-summary-${stat.description}`, + }; + + if (stat.fields != null) + statItemProps = { + ...statItemProps, + fields: addValueToFields(stat.fields, data), + }; + + if (stat.enableAreaChart) + statItemProps = { + ...statItemProps, + areaChart: addValueToAreaChart(stat.fields, data), + }; + + if (stat.enableBarChart != null) + statItemProps = { + ...statItemProps, + barChart: addValueToBarChart(stat.fields, data), + }; + + return ; + })} + + ); +}); + +const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => + fields.map(field => ({ ...field, value: get(field.key, data) })); + +const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartData[] => + fields + .filter(field => get(`${field.key}Histogram`, data) != null) + .map(field => ({ + ...field, + value: get(`${field.key}Histogram`, data), + key: `${field.key}Histogram`, + })); + +const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { + if (fields.length === 0) return []; + return fields.reduce((acc: BarChartData[], field: StatItem, idx: number) => { + const key: string = get('key', field); + const x: number | null = getOr(null, key, data); + const y: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields); + + return acc.concat([ + { + ...field, + value: [ + { + x, + y, + }, + ], + }, + ]); + }, []); +}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts new file mode 100644 index 0000000000000..366f2fabb734d --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts @@ -0,0 +1,58 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const HOSTS = i18n.translate('xpack.siem.kpiHosts.source.hostsTitle', { + defaultMessage: 'Hosts', +}); + +export const AGENTS = i18n.translate('xpack.siem.kpiHosts.source.agentsTitle', { + defaultMessage: 'Agents', +}); + +export const AUTHENTICATION_SUCCESS = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationSuccessTitle', + { + defaultMessage: 'Success', + } +); + +export const AUTHENTICATION_FAILURE = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationFailureTitle', + { + defaultMessage: 'Fail', + } +); + +export const AUTHENTICATION = i18n.translate('xpack.siem.kpiHosts.source.authenticationTitle', { + defaultMessage: 'User Authentications', +}); + +export const ACTIVE_USERS = i18n.translate('xpack.siem.kpiHosts.source.activeUsersTitle', { + defaultMessage: 'Active Users', +}); + +export const UNIQUE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueIpsTitle', { + defaultMessage: 'Unique IPs', +}); + +export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueSourceIpsTitle', { + defaultMessage: 'Source', +}); + +export const UNIQUE_SOURCE_IPS_ABBREVIATION = i18n.translate( + 'xpack.siem.kpiHosts.source.uniqueSourceIpsAbbreviationTitle', + { + defaultMessage: 'Src.', + } +); + +export const UNIQUE_DESTINATION_IPS = i18n.translate( + 'xpack.siem.kpiHosts.source.uniqueDestinationIpsTitle', + { + defaultMessage: 'Destination', + } +); diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx index cbf4edb4eb9b7..4312b3ecd4629 100644 --- a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx +++ b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx @@ -9,7 +9,9 @@ import { get } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; -import { CardItem, CardItems, CardItemsComponent } from '../../../../components/card_items'; +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import styled from 'styled-components'; +import { StatItem, StatItems, StatItemsComponent } from '../../../../components/stat_items'; import { KpiNetworkData } from '../../../../graphql/types'; import * as i18n from './translations'; @@ -19,86 +21,95 @@ interface KpiNetworkProps { loading: boolean; } -const fieldTitleMapping: Readonly = [ +const fieldTitleMapping: Readonly = [ { fields: [ { key: 'networkEvents', - description: i18n.NETWORK_EVENTS, value: null, }, ], + description: i18n.NETWORK_EVENTS, }, { fields: [ { key: 'uniqueFlowId', - description: i18n.UNIQUE_ID, value: null, }, ], + description: i18n.UNIQUE_ID, }, { fields: [ { key: 'activeAgents', - description: i18n.ACTIVE_AGENTS, value: null, }, ], + description: i18n.ACTIVE_AGENTS, }, { fields: [ { key: 'uniqueSourcePrivateIps', - description: i18n.UNIQUE_SOURCE_PRIVATE_IPS, value: null, }, ], + description: i18n.UNIQUE_SOURCE_PRIVATE_IPS, }, { fields: [ { key: 'uniqueDestinationPrivateIps', - description: i18n.UNIQUE_DESTINATION_PRIVATE_IPS, value: null, }, ], + description: i18n.UNIQUE_DESTINATION_PRIVATE_IPS, }, { fields: [ { key: 'dnsQueries', - description: i18n.DNS_QUERIES, value: null, }, ], + description: i18n.DNS_QUERIES, }, { fields: [ { key: 'tlsHandshakes', - description: i18n.TLS_HANDSHAKES, value: null, }, ], + description: i18n.TLS_HANDSHAKES, }, ]; +const FlexGroup = styled(EuiFlexGroup)` + margin-height: 86px; +`; + export const KpiNetworkComponent = pure(({ data, loading }) => { - return ( + return loading ? ( + + + + + + ) : ( - {fieldTitleMapping.map(card => ( - ( + ))} ); }); -const addValueToFields = (fields: CardItem[], data: KpiNetworkData): CardItem[] => +const addValueToFields = (fields: StatItem[], data: KpiNetworkData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); diff --git a/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..e2740c7d16ed9 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -0,0 +1,1131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Stat Items disable charts it renders the default widget 1`] = ` + + + + + + + + + + HOSTS + + + + + + + + + + + + + + + + -- + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`Stat Items disable charts it renders the default widget 2`] = ` + + + + + + + + + + HOSTS + + + + + + + + + + 0 + + + + + + + -- + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`Stat Items rendering kpis with charts it renders the default widget 1`] = ` + + + + + + + + + + UNIQUE_PRIVATE_IPS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1,714 + + Source + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2,359 + + Dest. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx new file mode 100644 index 0000000000000..a00bb19027748 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx @@ -0,0 +1,202 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import * as React from 'react'; + +import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart'; +import { AreaChartData } from '.'; + +describe('AreaChartBaseComponent', () => { + let wrapper: ReactWrapper; + const mockAreaChartData: AreaChartData[] = [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: 1096175 }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ]; + + describe('render', () => { + beforeAll(() => { + wrapper = mount(); + }); + + it('should render two area series', () => { + expect(wrapper.find('EuiAreaSeries')).toHaveLength(2); + }); + + it('should render a customized x-asix', () => { + expect(wrapper.find('EuiXAxis')).toHaveLength(1); + }); + + it('should render a customized y-asix', () => { + expect(wrapper.find('EuiYAxis')).toHaveLength(1); + }); + }); + + describe('no render', () => { + beforeAll(() => { + wrapper = mount( + + ); + }); + + it('should not render without height and width', () => { + expect(wrapper.find('SeriesChart')).toHaveLength(0); + }); + }); +}); + +describe('AreaChartWithCustomPrompt', () => { + let wrapper: ReactWrapper; + describe.each([ + [ + [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: 1096175 }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], + [ + [ + { + key: 'uniqueSourceIpsHistogram', + value: [], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], + ], + ], + ])('renders areachart', (data: AreaChartData[] | [] | null | undefined) => { + beforeAll(() => { + wrapper = mount(); + }); + + it('render AreaChartBaseComponent', () => { + expect(wrapper.find('[data-test-subj="stat-area-chart"]').first()).toHaveLength(1); + expect(wrapper.find('ChartHolder')).toHaveLength(0); + }); + }); + + describe.each([ + null, + [], + [ + { + key: 'uniqueSourceIpsHistogram', + value: null, + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: null, + color: '#490092', + }, + ], + [ + { + key: 'uniqueSourceIpsHistogram', + value: [{ x: 1556686800000 }, { x: 1556730000000 }, { x: 1556773200000 }], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [{ x: 1556686800000 }, { x: 1556730000000 }, { x: 1556773200000 }], + color: '#490092', + }, + ], + [ + [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: null }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], + ], + [ + [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: {} }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], + ], + ])('renders prompt', (data: AreaChartData[] | [] | null | undefined) => { + beforeAll(() => { + wrapper = mount(); + }); + + it('render Chart Holder', () => { + expect(wrapper.find('[data-test-subj="stat-area-chart"]')).toHaveLength(0); + expect(wrapper.find('ChartHolder')).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx new file mode 100644 index 0000000000000..3dc6782f704f5 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -0,0 +1,85 @@ +/* + * 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 React from 'react'; +import { pure } from 'recompose'; +import styled from 'styled-components'; +import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; +import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.'; +import { AutoSizer } from '../auto_sizer'; + +export const AreaChartBaseComponent = pure<{ + data: AreaChartData[]; + width: number | null | undefined; + height: number | null | undefined; +}>(({ data, ...chartConfigs }) => + chartConfigs.width && chartConfigs.height ? ( + + {data.map(series => ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + + ))} + {/* +// @ts-ignore */} + timestamp.split('T')[0]} /> + {/* +// @ts-ignore */} + + + ) : null +); + +export const AreaChartWithCustomPrompt = pure<{ + data: AreaChartData[] | null | undefined; + height: number | null | undefined; + width: number | null | undefined; +}>(({ data, height, width }) => { + return data != null && + data.length && + data.every( + ({ value }) => + value != null && + value.length > 0 && + value.every(chart => chart.x != null && chart.y != null) + ) ? ( + + ) : ( + + ); +}); + +export const AreaChart = pure<{ areaChart: AreaChartData[] | null | undefined }>( + ({ areaChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + + ) +); + +const SeriesChart = styled(EuiSeriesChart)` + svg .rv-xy-plot__axis__ticks .rv-xy-plot__axis__tick:not(:first-child):not(:last-child) { + display: none; + } +`; diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx new file mode 100644 index 0000000000000..3edcb316526e7 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx @@ -0,0 +1,162 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import * as React from 'react'; + +import { BarChartBaseComponent, BarChartWithCustomPrompt } from './barchart'; +import { BarChartData } from '.'; + +describe('BarChartBaseComponent', () => { + let wrapper: ReactWrapper; + const mockBarChartData: BarChartData[] = [ + { key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ]; + + describe('render', () => { + beforeAll(() => { + wrapper = mount(); + }); + + it('should render two area series', () => { + expect(wrapper.find('EuiBarSeries')).toHaveLength(2); + }); + + it('should render a customized x-asix', () => { + expect(wrapper.find('EuiXAxis')).toHaveLength(1); + }); + + it('should render a customized y-asix', () => { + expect(wrapper.find('EuiYAxis')).toHaveLength(1); + }); + }); + + describe('no render', () => { + beforeAll(() => { + wrapper = mount(); + }); + + it('should not render without height and width', () => { + expect(wrapper.find('SeriesChart')).toHaveLength(0); + }); + }); +}); + +describe.each([ + [ + [ + { key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [{ x: 1714, y: '' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: '' }], + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [{ x: 0, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 0, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + ], +])('BarChartWithCustomPrompt', mockBarChartData => { + let wrapper: ReactWrapper; + describe('renders barchart', () => { + beforeAll(() => { + wrapper = mount( + + ); + }); + + it('render BarChartBaseComponent', () => { + expect(wrapper.find('[data-test-subj="stat-bar-chart"]').first()).toHaveLength(1); + expect(wrapper.find('ChartHolder')).toHaveLength(0); + }); + }); +}); + +describe.each([ + [], + null, + [ + [ + { key: 'uniqueSourceIps', color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [], + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [{}], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{}], + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + ], + [ + [ + { key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: null, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + ], +])('renders prompt', (data: BarChartData[] | [] | null | undefined) => { + let wrapper: ReactWrapper; + beforeAll(() => { + wrapper = mount(); + }); + + it('render Chart Holder', () => { + expect(wrapper.find('[data-test-subj="stat-bar-chart"]')).toHaveLength(0); + expect(wrapper.find('ChartHolder')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx new file mode 100644 index 0000000000000..10987e0b44449 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -0,0 +1,93 @@ +/* + * 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 React from 'react'; +import { + // @ts-ignore + EuiSeriesChartUtils, +} from '@elastic/eui'; +import { pure } from 'recompose'; +import styled from 'styled-components'; +import { EuiSeriesChart, EuiBarSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; +import { BarChartData, WrappedByAutoSizer, ChartHolder } from '.'; +import { AutoSizer } from '../auto_sizer'; + +const { SCALE, ORIENTATION } = EuiSeriesChartUtils; +const getYaxis = (value: string | number) => { + const label = value.toString(); + const labelLength = 4; + return label.length > labelLength ? `${label.slice(0, labelLength)}.` : label; +}; + +export const BarChartBaseComponent = pure<{ + data: BarChartData[]; + width: number | null | undefined; + height: number | null | undefined; +}>(({ data, ...chartConfigs }) => { + return chartConfigs.width && chartConfigs.height ? ( + // @ts-ignore + + {data.map(series => { + return ( + + ); + })} + {/* +// @ts-ignore */} + + {/* +// @ts-ignore */} + + + ) : null; +}); + +export const BarChartWithCustomPrompt = pure<{ + data: BarChartData[] | null | undefined; + height: number | null | undefined; + width: number | null | undefined; +}>(({ data, height, width }) => { + return data && + data.length && + data.every( + ({ value }) => value != null && value.length > 0 && value.every(chart => chart.x != null) + ) ? ( + + ) : ( + + ); +}); + +export const BarChart = pure<{ barChart: BarChartData[] | null | undefined }>(({ barChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + +)); + +const SeriesChart = styled(EuiSeriesChart)` + svg + .rv-xy-plot__axis--horizontal + .rv-xy-plot__axis__ticks + .rv-xy-plot__axis__tick:not(:first-child):not(:last-child) { + display: none; + } +`; diff --git a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx new file mode 100644 index 0000000000000..39b8b15c39b48 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -0,0 +1,143 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; + +import { StatItemsComponent, StatItemsProps } from '.'; +import { BarChart } from './barchart'; +import { AreaChart } from './areachart'; +import { EuiHorizontalRule } from '@elastic/eui'; + +describe('Stat Items', () => { + describe.each([ + [ + mount( + + ), + ], + [ + mount( + + ), + ], + ])('disable charts', wrapper => { + test('it renders the default widget', () => { + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + test('should render titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).toBeTruthy(); + }); + + test('should not render icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(0); + }); + + test('should not render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(0); + }); + + test('should not render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(0); + }); + + test('should not render spliter', () => { + expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0); + }); + }); + + describe('rendering kpis with charts', () => { + const mockStatItemsData: StatItemsProps = { + fields: [ + { + key: 'uniqueSourceIps', + description: 'Source', + value: 1714, + color: '#DB1374', + icon: 'cross', + }, + { + key: 'uniqueDestinationIps', + description: 'Dest.', + value: 2359, + color: '#490092', + icon: 'cross', + }, + ], + enableAreaChart: true, + enableBarChart: true, + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: 1096175 }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], + barChart: [ + { key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + key: 'mock-keys', + }; + let wrapper: ReactWrapper; + beforeAll(() => { + wrapper = mount(); + }); + test('it renders the default widget', () => { + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + test('should handle multiple titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).toHaveLength(2); + }); + + test('should render kpi icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(2); + }); + + test('should render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(1); + }); + + test('should render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(1); + }); + + test('should render separator', () => { + expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx new file mode 100644 index 0000000000000..ac4bf34bf2f6e --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -0,0 +1,162 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiHorizontalRule, + EuiIcon, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import { pure } from 'recompose'; +import styled from 'styled-components'; + +import { EuiText } from '@elastic/eui'; +import { BarChart } from './barchart'; +import { AreaChart } from './areachart'; +import { getEmptyTagValue } from '../empty_value'; + +export const WrappedByAutoSizer = styled.div` + height: 100px; + position: relative; + + &:hover { + z-index: 100; + } +`; + +const FlexGroup = styled(EuiFlexGroup)` + height: '100%'; +`; + +const FlexItem = styled(EuiFlexItem)` + min-width: 0; +`; + +const StatValue = styled(EuiTitle)` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export interface StatItem { + key: string; + description?: string; + value: number | undefined | null; + color?: string; + icon?: 'storage' | 'cross' | 'check' | 'visMapCoordinate'; + name?: string; +} + +export interface AreaChartData { + key: string; + value: ChartData[] | [] | null; + color?: string | undefined; +} + +export interface ChartData { + x: number | string | null; + y: number | string | null; + y0?: number; +} + +export interface BarChartData { + key: string; + value: ChartData[] | [] | null; + color?: string | undefined; +} + +export interface StatItems { + fields: StatItem[]; + description?: string; + enableAreaChart?: boolean; + enableBarChart?: boolean; + grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; +} + +export interface StatItemsProps extends StatItems { + key: string; + areaChart?: AreaChartData[]; + barChart?: BarChartData[]; +} + +export const StatItemsComponent = pure( + ({ fields, description, key, grow, barChart, areaChart, enableAreaChart, enableBarChart }) => { + const isBarChartDataAbailable = + barChart && + barChart.length && + barChart.every(item => item.value != null && item.value.length > 0); + const isAreaChartDataAvailable = + areaChart && + areaChart.length && + areaChart.every(item => item.value != null && item.value.length > 0); + return ( + + + + {description} + + + + {fields.map(field => ( + + + {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon && ( + + + + )} + + + + + {field.value ? field.value.toLocaleString() : getEmptyTagValue()}{' '} + {field.description} + + + + + + ))} + + + {(enableAreaChart || enableBarChart) && } + + + {enableBarChart && ( + + + + )} + + {enableAreaChart && ( + + + + )} + + + + ); + } +); + +export const ChartHolder = () => ( + + + + Chart Data Not Available + + + +); diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts new file mode 100644 index 0000000000000..97bcb9d7907c1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts @@ -0,0 +1,45 @@ +/* + * 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 kpiHostsQuery = gql` + fragment ChartFields on HistogramData { + x: key_as_string + y: count { + value + doc_count + } + } + + query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) { + source(id: $sourceId) { + id + KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { + hosts + hostsHistogram { + ...ChartFields + } + authSuccess + authSuccessHistogram { + ...ChartFields + } + authFailure + authFailureHistogram { + ...ChartFields + } + uniqueSourceIps + uniqueSourceIpsHistogram { + ...ChartFields + } + uniqueDestinationIps + uniqueDestinationIpsHistogram { + ...ChartFields + } + } + } + } +`; diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx new file mode 100644 index 0000000000000..5ee3623015b91 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, get } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { pure } from 'recompose'; + +import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; +import { inputsModel } from '../../store'; +import { createFilter } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiHostsQuery } from './index.gql_query'; +import { ChartData } from '../../components/stat_items'; + +export interface KpiHostsArgs { + id: string; + kpiHosts: KpiHostsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiHostsProps extends QueryTemplateProps { + children: (args: KpiHostsArgs) => React.ReactNode; +} + +const formatHistogramData = ( + data: Array<{ + x: number; + y: { value: number; doc_count: number }; + }> +): ChartData[] => { + return data.map(({ x, y }) => ({ + x, + y: y.value || y.doc_count, + })); +}; + +export const KpiHostsQuery = pure( + ({ id = 'kpiHostsQuery', children, filterQuery, sourceId, startDate, endDate }) => ( + + query={kpiHostsQuery} + fetchPolicy="cache-and-network" + notifyOnNetworkStatusChange + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + }} + > + {({ data, loading, refetch }) => { + const kpiHosts = getOr({}, `source.KpiHosts`, data); + const hostsHistogram = get(`hostsHistogram`, kpiHosts); + const authFailureHistogram = get(`authFailureHistogram`, kpiHosts); + const authSuccessHistogram = get(`authSuccessHistogram`, kpiHosts); + const uniqueSourceIpsHistogram = get(`uniqueSourceIpsHistogram`, kpiHosts); + const uniqueDestinationIpsHistogram = get(`uniqueDestinationIpsHistogram`, kpiHosts); + return children({ + id, + kpiHosts: { + ...kpiHosts, + hostsHistogram: hostsHistogram ? formatHistogramData(hostsHistogram) : [], + authFailureHistogram: authFailureHistogram + ? formatHistogramData(authFailureHistogram) + : [], + authSuccessHistogram: authSuccessHistogram + ? formatHistogramData(authSuccessHistogram) + : [], + uniqueSourceIpsHistogram: uniqueSourceIpsHistogram + ? formatHistogramData(uniqueSourceIpsHistogram) + : [], + uniqueDestinationIpsHistogram: uniqueDestinationIpsHistogram + ? formatHistogramData(uniqueDestinationIpsHistogram) + : [], + }, + loading, + refetch, + }); + }} + + ) +); diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index 6d8e7a60d7b6d..f951d737c47d1 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -797,6 +797,41 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "KpiHosts", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "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 + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "KpiHostsData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "NetworkTopNFlow", "description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified", @@ -6225,6 +6260,179 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "KpiHostsData", + "description": "", + "fields": [ + { + "name": "hosts", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostsHistogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authSuccess", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authSuccessHistogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authFailure", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authFailureHistogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueSourceIps", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueSourceIpsHistogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueDestinationIps", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueDestinationIpsHistogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "HistogramData", + "description": "", + "fields": [ + { + "name": "key", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "key_as_string", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "count", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Count", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Count", + "description": "", + "fields": [ + { + "name": "value", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "doc_count", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "NetworkTopNFlowSortField", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 8adb52fa2ba80..2dcc8bc0328a1 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -72,6 +72,8 @@ export interface Source { Users: UsersData; KpiNetwork?: KpiNetworkData | null; + + KpiHosts: KpiHostsData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1032,6 +1034,42 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + hostsHistogram?: (HistogramData | null)[] | null; + + authSuccess?: number | null; + + authSuccessHistogram?: (HistogramData | null)[] | null; + + authFailure?: number | null; + + authFailureHistogram?: (HistogramData | null)[] | null; + + uniqueSourceIps?: number | null; + + uniqueSourceIpsHistogram?: (HistogramData | null)[] | null; + + uniqueDestinationIps?: number | null; + + uniqueDestinationIpsHistogram?: (HistogramData | null)[] | null; +} + +export interface HistogramData { + key?: number | null; + + key_as_string?: string | null; + + count?: Count | null; +} + +export interface Count { + value?: number | null; + + doc_count?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1407,6 +1445,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { id?: string | null; @@ -2403,6 +2448,62 @@ export namespace GetIpOverviewQuery { }; } +export namespace GetKpiHostsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiHosts: KpiHosts; + }; + + export type KpiHosts = { + __typename?: 'KpiHostsData'; + + hosts?: number | null; + + hostsHistogram?: (HostsHistogram | null)[] | null; + + authSuccess?: number | null; + + authSuccessHistogram?: (AuthSuccessHistogram | null)[] | null; + + authFailure?: number | null; + + authFailureHistogram?: (AuthFailureHistogram | null)[] | null; + + uniqueSourceIps?: number | null; + + uniqueSourceIpsHistogram?: (UniqueSourceIpsHistogram | null)[] | null; + + uniqueDestinationIps?: number | null; + + uniqueDestinationIpsHistogram?: (UniqueDestinationIpsHistogram | null)[] | null; + }; + + export type HostsHistogram = ChartFields.Fragment; + + export type AuthSuccessHistogram = ChartFields.Fragment; + + export type AuthFailureHistogram = ChartFields.Fragment; + + export type UniqueSourceIpsHistogram = ChartFields.Fragment; + + export type UniqueDestinationIpsHistogram = ChartFields.Fragment; +} + export namespace GetKpiNetworkQuery { export type Variables = { sourceId: string; @@ -3785,3 +3886,21 @@ export namespace GetUsersQuery { value: string; }; } + +export namespace ChartFields { + export type Fragment = { + __typename?: 'HistogramData'; + + x?: string | null; + + y?: Y | null; + }; + + export type Y = { + __typename?: 'Count'; + + value?: number | null; + + doc_count?: number | null; + }; +} diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx index ab1dc941bb3ea..5c9609941d61e 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx @@ -12,13 +12,19 @@ import { pure } from 'recompose'; import chrome from 'ui/chrome'; import { EmptyPage } from '../../components/empty_page'; -import { EventsTable, HostsTable, UncommonProcessTable } from '../../components/page/hosts'; +import { + EventsTable, + HostsTable, + KpiHostsComponent, + UncommonProcessTable, +} from '../../components/page/hosts'; import { AuthenticationTable } from '../../components/page/hosts/authentications_table'; import { manageQuery } from '../../components/page/manage_query'; import { AuthenticationsQuery } from '../../containers/authentications'; import { EventsQuery } from '../../containers/events'; import { GlobalTime } from '../../containers/global_time'; import { HostsQuery } from '../../containers/hosts'; +import { KpiHostsQuery } from '../../containers/kpi_hosts'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { UncommonProcessesQuery } from '../../containers/uncommon_processes'; import { IndexType } from '../../graphql/types'; @@ -33,7 +39,7 @@ const AuthenticationTableManage = manageQuery(AuthenticationTable); const HostsTableManage = manageQuery(HostsTable); const EventsTableManage = manageQuery(EventsTable); const UncommonProcessTableManage = manageQuery(UncommonProcessTable); - +const KpiHostsComponentManage = manageQuery(KpiHostsComponent); interface HostsComponentReduxProps { filterQuery: string; } @@ -51,6 +57,25 @@ const HostsComponent = pure(({ filterQuery }) => ( {({ to, from, setQuery }) => ( <> + + {({ kpiHosts, loading, id, refetch }) => ( + + )} + + + + , + QuerySourceResolver +>; + +export interface KpiHostsResolversDeps { + kpiHosts: KpiHosts; +} + +export const createKpiHostsResolvers = ( + libs: KpiHostsResolversDeps +): { + Source: { + KpiHosts: QueryKpiHostsResolver; + }; +} => ({ + Source: { + async KpiHosts(source, args, { req }, info) { + const options = { ...createOptions(source, args, info) }; + return libs.kpiHosts.getKpiHosts(req, options); + }, + }, +}); diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts new file mode 100644 index 0000000000000..85c98e89f9313 --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.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 kpiHostsSchema = gql` + type Count { + value: Float + doc_count: Float + } + + type HistogramData { + key: Float + key_as_string: String + count: Count + } + + type KpiHostsData { + hosts: Float + hostsHistogram: [HistogramData] + authSuccess: Float + authSuccessHistogram: [HistogramData] + authFailure: Float + authFailureHistogram: [HistogramData] + uniqueSourceIps: Float + uniqueSourceIpsHistogram: [HistogramData] + uniqueDestinationIps: Float + uniqueDestinationIpsHistogram: [HistogramData] + } + + extend type Source { + KpiHosts(id: String, timerange: TimerangeInput!, filterQuery: String): KpiHostsData! + } +`; diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index 0eff3e1aa577b..721ddce7e7c36 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -101,6 +101,8 @@ export interface Source { Users: UsersData; KpiNetwork?: KpiNetworkData | null; + + KpiHosts: KpiHostsData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1061,6 +1063,42 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + hostsHistogram?: (HistogramData | null)[] | null; + + authSuccess?: number | null; + + authSuccessHistogram?: (HistogramData | null)[] | null; + + authFailure?: number | null; + + authFailureHistogram?: (HistogramData | null)[] | null; + + uniqueSourceIps?: number | null; + + uniqueSourceIpsHistogram?: (HistogramData | null)[] | null; + + uniqueDestinationIps?: number | null; + + uniqueDestinationIpsHistogram?: (HistogramData | null)[] | null; +} + +export interface HistogramData { + key?: number | null; + + key_as_string?: string | null; + + count?: Count | null; +} + +export interface Count { + value?: number | null; + + doc_count?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1436,6 +1474,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { id?: string | null; @@ -1642,6 +1687,8 @@ export namespace SourceResolvers { Users?: UsersResolver; KpiNetwork?: KpiNetworkResolver; + + KpiHosts?: KpiHostsResolver; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow?: NetworkTopNFlowResolver; @@ -1894,6 +1941,20 @@ export namespace SourceResolvers { filterQuery?: string | null; } + export type KpiHostsResolver = Resolver< + R, + Parent, + Context, + KpiHostsArgs + >; + export interface KpiHostsArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; + } + export type NetworkTopNFlowResolver< R = NetworkTopNFlowData, Parent = Source, @@ -5122,6 +5183,142 @@ export namespace KpiNetworkDataResolvers { > = Resolver; } +export namespace KpiHostsDataResolvers { + export interface Resolvers { + hosts?: HostsResolver; + + hostsHistogram?: HostsHistogramResolver<(HistogramData | null)[] | null, TypeParent, Context>; + + authSuccess?: AuthSuccessResolver; + + authSuccessHistogram?: AuthSuccessHistogramResolver< + (HistogramData | null)[] | null, + TypeParent, + Context + >; + + authFailure?: AuthFailureResolver; + + authFailureHistogram?: AuthFailureHistogramResolver< + (HistogramData | null)[] | null, + TypeParent, + Context + >; + + uniqueSourceIps?: UniqueSourceIpsResolver; + + uniqueSourceIpsHistogram?: UniqueSourceIpsHistogramResolver< + (HistogramData | null)[] | null, + TypeParent, + Context + >; + + uniqueDestinationIps?: UniqueDestinationIpsResolver; + + uniqueDestinationIpsHistogram?: UniqueDestinationIpsHistogramResolver< + (HistogramData | null)[] | null, + TypeParent, + Context + >; + } + + export type HostsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type HostsHistogramResolver< + R = (HistogramData | null)[] | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthSuccessResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthSuccessHistogramResolver< + R = (HistogramData | null)[] | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthFailureResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthFailureHistogramResolver< + R = (HistogramData | null)[] | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueSourceIpsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueSourceIpsHistogramResolver< + R = (HistogramData | null)[] | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueDestinationIpsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueDestinationIpsHistogramResolver< + R = (HistogramData | null)[] | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; +} + +export namespace HistogramDataResolvers { + export interface Resolvers { + key?: KeyResolver; + + key_as_string?: KeyAsStringResolver; + + count?: CountResolver; + } + + export type KeyResolver< + R = number | null, + Parent = HistogramData, + Context = SiemContext + > = Resolver; + export type KeyAsStringResolver< + R = string | null, + Parent = HistogramData, + Context = SiemContext + > = Resolver; + export type CountResolver< + R = Count | null, + Parent = HistogramData, + Context = SiemContext + > = Resolver; +} + +export namespace CountResolvers { + export interface Resolvers { + value?: ValueResolver; + + doc_count?: DocCountResolver; + } + + export type ValueResolver = Resolver< + R, + Parent, + Context + >; + export type DocCountResolver = Resolver< + R, + Parent, + Context + >; +} + export namespace NetworkTopNFlowDataResolvers { export interface Resolvers { edges?: EdgesResolver; diff --git a/x-pack/plugins/siem/server/init_server.ts b/x-pack/plugins/siem/server/init_server.ts index 071e4c048a848..f571f0e4d9551 100644 --- a/x-pack/plugins/siem/server/init_server.ts +++ b/x-pack/plugins/siem/server/init_server.ts @@ -11,6 +11,7 @@ import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; import { createHostsResolvers } from './graphql/hosts'; import { createIpDetailsResolvers } from './graphql/ip_details'; +import { createKpiHostsResolvers } from './graphql/kpi_hosts'; import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; import { createOverviewResolvers } from './graphql/overview'; @@ -52,6 +53,7 @@ export const initServer = (libs: AppBackendLibs, config: Config) => { createUncommonProcessesResolvers(libs) as IResolvers, createWhoAmIResolvers() as IResolvers, createKpiNetworkResolvers(libs) as IResolvers, + createKpiHostsResolvers(libs) as IResolvers, ], typeDefs: schemas, }); diff --git a/x-pack/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index cc71cf73b4237..4ed99a085b081 100644 --- a/x-pack/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/plugins/siem/server/lib/compose/kibana.ts @@ -12,6 +12,9 @@ import { KibanaConfigurationAdapter } from '../configuration/kibana_configuratio import { ElasticsearchEventsAdapter, Events } from '../events'; import { KibanaBackendFrameworkAdapter } from '../framework/kibana_framework_adapter'; import { ElasticsearchHostsAdapter, Hosts } from '../hosts'; +import { KpiHosts } from '../kpi_hosts'; +import { ElasticsearchKpiHostsAdapter } from '../kpi_hosts/elasticsearch_adapter'; + import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; import { ElasticsearchIpOverviewAdapter, IpDetails } from '../ip_details'; import { KpiNetwork } from '../kpi_network'; @@ -36,6 +39,7 @@ export function compose(server: Server): AppBackendLibs { fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework), sources), hosts: new Hosts(new ElasticsearchHostsAdapter(framework)), ipDetails: new IpDetails(new ElasticsearchIpOverviewAdapter(framework)), + kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), overview: new Overview(new ElasticsearchOverviewAdapter(framework)), diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts new file mode 100644 index 0000000000000..fc413d350a589 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -0,0 +1,121 @@ +/* + * 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 { KpiHostsData } from '../../graphql/types'; +import { FrameworkAdapter, FrameworkRequest } from '../framework'; + +import { ElasticsearchKpiHostsAdapter } from './elasticsearch_adapter'; +import { + mockAuthQuery, + mockGeneralQuery, + mockMsearchOptions, + mockOptions, + mockRequest, + mockResponse, + mockResult, +} from './mock'; +import * as authQueryDsl from './query_authentication.dsl'; +import * as generalQueryDsl from './query_general.dsl'; + +describe('Hosts Kpi elasticsearch_adapter', () => { + const mockCallWithRequest = jest.fn(); + const mockFramework: FrameworkAdapter = { + version: 'mock', + callWithRequest: mockCallWithRequest, + exposeStaticDir: jest.fn(), + registerGraphQLEndpoint: jest.fn(), + getIndexPatternsService: jest.fn(), + }; + let mockBuildQuery: jest.SpyInstance; + let mockBuildAuthQuery: jest.SpyInstance; + let EsKpiHosts: ElasticsearchKpiHostsAdapter; + let data: KpiHostsData; + + describe('getKpiHosts - call stack', () => { + beforeAll(async () => { + mockCallWithRequest.mockResolvedValue(mockResponse); + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + mockBuildQuery = jest + .spyOn(generalQueryDsl, 'buildGeneralQuery') + .mockReturnValue(mockGeneralQuery); + mockBuildAuthQuery = jest + .spyOn(authQueryDsl, 'buildAuthQuery') + .mockReturnValue(mockAuthQuery); + + EsKpiHosts = new ElasticsearchKpiHostsAdapter(mockFramework); + data = await EsKpiHosts.getKpiHosts(mockRequest as FrameworkRequest, mockOptions); + }); + + afterAll(() => { + mockCallWithRequest.mockReset(); + mockBuildQuery.mockRestore(); + mockBuildAuthQuery.mockRestore(); + }); + + test('should build general query with correct option', () => { + expect(mockBuildQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should build auth query with correct option', () => { + expect(mockBuildAuthQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should send msearch request', () => { + expect(mockCallWithRequest).toHaveBeenCalledWith(mockRequest, 'msearch', mockMsearchOptions); + }); + }); + + describe('Happy Path - get Data', () => { + beforeAll(async () => { + mockCallWithRequest.mockResolvedValue(mockResponse); + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + EsKpiHosts = new ElasticsearchKpiHostsAdapter(mockFramework); + data = await EsKpiHosts.getKpiHosts(mockRequest as FrameworkRequest, mockOptions); + }); + + afterAll(() => { + mockCallWithRequest.mockReset(); + }); + + test('getKpiHosts - response with data', () => { + expect(data).toEqual(mockResult); + }); + }); + + describe('Unhappy Path - No data', () => { + beforeAll(async () => { + mockCallWithRequest.mockResolvedValue(null); + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + EsKpiHosts = new ElasticsearchKpiHostsAdapter(mockFramework); + data = await EsKpiHosts.getKpiHosts(mockRequest as FrameworkRequest, mockOptions); + }); + + afterAll(() => { + mockCallWithRequest.mockReset(); + }); + + test('getKpiHosts - response without data', async () => { + expect(data).toEqual({ + hosts: null, + hostsHistogram: null, + authSuccess: null, + authSuccessHistogram: null, + authFailure: null, + authFailureHistogram: null, + uniqueSourceIps: null, + uniqueSourceIpsHistogram: null, + uniqueDestinationIps: null, + uniqueDestinationIpsHistogram: null, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..57caf40caa2f0 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -0,0 +1,78 @@ +/* + * 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 { KpiHostsData } from '../../graphql/types'; +import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TermAggregation } from '../types'; + +import { buildAuthQuery } from './query_authentication.dsl'; +import { buildGeneralQuery } from './query_general.dsl'; +import { + KpiHostsAdapter, + KpiHostsESMSearchBody, + KpiHostsGeneralHit, + KpiHostsAuthHit, +} from './types'; + +export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getKpiHosts( + request: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + const generalQuery: KpiHostsESMSearchBody[] = buildGeneralQuery(options); + const authQuery: KpiHostsESMSearchBody[] = buildAuthQuery(options); + const response = await this.framework.callWithRequest< + KpiHostsGeneralHit | KpiHostsAuthHit, + TermAggregation + >(request, 'msearch', { + body: [...generalQuery, ...authQuery], + }); + return { + hosts: getOr(null, 'responses.0.aggregations.hosts.value', response), + hostsHistogram: getOr(null, 'responses.0.aggregations.hosts_histogram.buckets', response), + authSuccess: getOr( + null, + 'responses.1.aggregations.authentication_success.doc_count', + response + ), + authSuccessHistogram: getOr( + null, + 'responses.1.aggregations.authentication_success_histogram.buckets', + response + ), + authFailure: getOr( + null, + 'responses.1.aggregations.authentication_failure.doc_count', + response + ), + authFailureHistogram: getOr( + null, + 'responses.1.aggregations.authentication_failure_histogram.buckets', + response + ), + uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response), + uniqueSourceIpsHistogram: getOr( + null, + 'responses.0.aggregations.unique_source_ips_histogram.buckets', + response + ), + uniqueDestinationIps: getOr( + null, + 'responses.0.aggregations.unique_destination_ips.value', + response + ), + uniqueDestinationIpsHistogram: getOr( + null, + 'responses.0.aggregations.unique_destination_ips_histogram.buckets', + response + ), + }; + } +} diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/index.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/index.ts new file mode 100644 index 0000000000000..2b61d07922a08 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { KpiHostsData } from '../../graphql/types'; +import { FrameworkRequest, RequestBasicOptions } from '../framework'; + +import { KpiHostsAdapter } from './types'; + +export class KpiHosts { + constructor(private readonly adapter: KpiHostsAdapter) {} + + public async getKpiHosts( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + return await this.adapter.getKpiHosts(req, options); + } +} diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts new file mode 100644 index 0000000000000..903c17dbe27d7 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -0,0 +1,455 @@ +/* + * 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 { RequestBasicOptions } from '../framework/types'; + +export const mockOptions: RequestBasicOptions = { + sourceConfiguration: { + logAlias: 'filebeat-*', + auditbeatAlias: 'auditbeat-*', + packetbeatAlias: 'packetbeat-*', + winlogbeatAlias: 'winlogbeat-*', + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1549852006071, from: 1549765606071 }, + filterQuery: {}, +}; + +export const mockRequest = { + params: {}, + payload: { + operationName: 'GetKpiHostsQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1556890277121, to: 1556976677122 }, + filterQuery: '', + }, + query: + 'fragment ChartFields on HistogramData {\n x: key\n y: count {\n value\n doc_count\n __typename\n }\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery) {\n hosts\n hostsHistogram {\n ...ChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...ChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...ChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...ChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...ChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, + query: {}, +}; + +export const mockResponse = { + took: 4405, + responses: [ + { + took: 1234, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 65, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + unique_destination_ips_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1809, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 407, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 64, + }, + }, + ], + interval: '12h', + }, + unique_source_ips: { + value: 1407, + }, + hosts: { + value: 986, + }, + unique_source_ips_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1182, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 364, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 63, + }, + }, + ], + interval: '12h', + }, + hosts_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 919, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 82, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 4, + }, + }, + ], + interval: '12h', + }, + unique_destination_ips: { + value: 1954, + }, + }, + status: 200, + }, + { + took: 320, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 65, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + authentication_success: { + doc_count: 61, + }, + authentication_failure: { + doc_count: 15722, + }, + authentication_failure_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 11731, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 3979, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 12, + }, + }, + ], + interval: '12h', + }, + authentication_success_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 8, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 52, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 1, + }, + }, + ], + interval: '12h', + }, + }, + status: 200, + }, + ], +}; + +export const mockResult = { + hosts: 986, + hostsHistogram: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 919, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 82, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 4, + }, + }, + ], + authSuccess: 61, + authSuccessHistogram: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 8, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 52, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 1, + }, + }, + ], + authFailure: 15722, + authFailureHistogram: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 11731, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 3979, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 12, + }, + }, + ], + uniqueSourceIps: 1407, + uniqueSourceIpsHistogram: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1182, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 364, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 63, + }, + }, + ], + uniqueDestinationIps: 1954, + uniqueDestinationIpsHistogram: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1809, + }, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 407, + }, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 64, + }, + }, + ], +}; + +export const mockGeneralQuery = [ + { + index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + aggregations: { + hosts: { cardinality: { field: 'host.name' } }, + hosts_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'host.name' } } }, + }, + unique_source_ips: { cardinality: { field: 'source.ip' } }, + unique_source_ips_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'source.ip' } } }, + }, + unique_destination_ips: { cardinality: { field: 'destination.ip' } }, + unique_destination_ips_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'destination.ip' } } }, + }, + }, + query: { + bool: { filter: [{ range: { '@timestamp': { gte: 1556889840660, lte: 1556976240660 } } }] }, + }, + size: 0, + track_total_hits: false, + }, +]; + +export const mockAuthQuery = [ + { + index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + aggs: { + authentication_success: { filter: { term: { 'event.type': 'authentication_success' } } }, + authentication_success_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { filter: { term: { 'event.type': 'authentication_success' } } } }, + }, + authentication_failure: { filter: { term: { 'event.type': 'authentication_failure' } } }, + authentication_failure_histogram: { + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { filter: { term: { 'event.type': 'authentication_failure' } } } }, + }, + }, + query: { + bool: { + filter: [ + { + bool: { + should: [ + { match: { 'event.type': 'authentication_success' } }, + { match: { 'event.type': 'authentication_failure' } }, + ], + minimum_should_match: 1, + }, + }, + { range: { '@timestamp': { gte: 1556889840660, lte: 1556976240660 } } }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, +]; + +export const mockMsearchOptions = { + body: [...mockGeneralQuery, ...mockAuthQuery], +}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts new file mode 100644 index 0000000000000..c9056eacd8c54 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts @@ -0,0 +1,119 @@ +/* + * 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 } from '../../utils/build_query'; +import { RequestBasicOptions } from '../framework'; + +import { KpiHostsESMSearchBody } from './types'; + +const getAuthQueryFilter = () => [ + { + bool: { + should: [ + { + match: { + 'event.type': 'authentication_success', + }, + }, + { + match: { + 'event.type': 'authentication_failure', + }, + }, + ], + minimum_should_match: 1, + }, + }, +]; + +export const buildAuthQuery = ({ + filterQuery, + timerange: { from, to }, + sourceConfiguration: { + fields: { timestamp }, + logAlias, + auditbeatAlias, + packetbeatAlias, + winlogbeatAlias, + }, +}: RequestBasicOptions): KpiHostsESMSearchBody[] => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + ...getAuthQueryFilter(), + { + range: { + [timestamp]: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = [ + { + index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + aggs: { + authentication_success: { + filter: { + term: { + 'event.type': 'authentication_success', + }, + }, + }, + authentication_success_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.type': 'authentication_success', + }, + }, + }, + }, + }, + authentication_failure: { + filter: { + term: { + 'event.type': 'authentication_failure', + }, + }, + }, + authentication_failure_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.type': 'authentication_failure', + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + ]; + + return dslQuery; +}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_general.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_general.dsl.ts new file mode 100644 index 0000000000000..52c9ccfa51767 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_general.dsl.ts @@ -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 { createQueryFilterClauses } from '../../utils/build_query'; +import { RequestBasicOptions } from '../framework'; + +import { KpiHostsESMSearchBody } from './types'; + +export const buildGeneralQuery = ({ + filterQuery, + timerange: { from, to }, + sourceConfiguration: { + fields: { timestamp }, + logAlias, + auditbeatAlias, + packetbeatAlias, + winlogbeatAlias, + }, +}: RequestBasicOptions): KpiHostsESMSearchBody[] => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + [timestamp]: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = [ + { + index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + aggregations: { + hosts: { + cardinality: { + field: 'host.name', + }, + }, + hosts_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + unique_source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + unique_source_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'source.ip', + }, + }, + }, + }, + unique_destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + unique_destination_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + ]; + + return dslQuery; +}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts new file mode 100644 index 0000000000000..e744811f243b0 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts @@ -0,0 +1,135 @@ +/* + * 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 { KpiHostsData } from '../../graphql/types'; +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { MSearchHeader, SearchHit } from '../types'; + +export interface KpiHostsAdapter { + getKpiHosts(request: FrameworkRequest, options: RequestBasicOptions): Promise; +} + +export interface KpiHostsGeneralHit extends SearchHit { + aggregations: { + hosts: { + value: number; + }; + hosts_histogram: { + buckets: [ + { + key_as_string: string; + key: number; + doc_count: number; + count: { + value: number; + }; + } + ]; + }; + unique_source_ips: { + value: number; + }; + unique_source_ips_histogram: { + buckets: [ + { + key_as_string: string; + key: number; + doc_count: number; + count: { + value: number; + }; + } + ]; + }; + unique_destination_ips: { + value: number; + }; + unique_destination_ips_histogram: { + buckets: [ + { + key_as_string: string; + key: number; + doc_count: number; + count: { + value: number; + }; + } + ]; + }; + }; + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + hits: { + max_score: number | null; + hits: []; + }; + took: number; + timeout: number; +} + +export interface KpiHostsAuthHit extends SearchHit { + aggregations: { + authentication_success: { + doc_count: number; + }; + authentication_success_histogram: { + buckets: [ + { + key_as_string: string; + key: number; + doc_count: number; + count: { + doc_count: number; + }; + } + ]; + }; + authentication_failure: { + doc_count: number; + }; + authentication_failure_histogram: { + buckets: [ + { + key_as_string: string; + key: number; + doc_count: number; + count: { + doc_count: number; + }; + } + ]; + }; + }; + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + hits: { + max_score: number | null; + hits: []; + }; + took: number; + timeout: number; +} + +export interface KpiHostsBody { + query?: object; + aggregations?: object; + size?: number; + track_total_hits?: boolean; +} + +export type KpiHostsESMSearchBody = KpiHostsBody | MSearchHeader; + +export interface EventModuleAttributeQuery { + agentType: 'auditbeat' | 'winlogbeat' | 'filebeat'; + eventModule?: 'file_integrity' | 'auditd'; +} diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts index 4f42c7633e100..22461bcc6230a 100644 --- a/x-pack/plugins/siem/server/lib/types.ts +++ b/x-pack/plugins/siem/server/lib/types.ts @@ -11,6 +11,7 @@ import { FrameworkAdapter, FrameworkRequest } from './framework'; import { Hosts } from './hosts'; import { IndexFields } from './index_fields'; import { IpDetails } from './ip_details'; +import { KpiHosts } from './kpi_hosts'; import { KpiNetwork } from './kpi_network'; import { Network } from './network'; import { Overview } from './overview'; @@ -30,6 +31,7 @@ export interface AppDomainLibs { kpiNetwork: KpiNetwork; overview: Overview; uncommonProcesses: UncommonProcesses; + kpiHosts: KpiHosts; } export interface AppBackendLibs extends AppDomainLibs { diff --git a/x-pack/plugins/siem/server/utils/build_query/fields.ts b/x-pack/plugins/siem/server/utils/build_query/fields.ts index 1b1e3085b6f98..574ec2d03bcf9 100644 --- a/x-pack/plugins/siem/server/utils/build_query/fields.ts +++ b/x-pack/plugins/siem/server/utils/build_query/fields.ts @@ -31,5 +31,6 @@ export const getFields = ( fields as string[] ); } + return fields; }; diff --git a/x-pack/test/api_integration/apis/siem/index.js b/x-pack/test/api_integration/apis/siem/index.js index 56b6b9f852835..f885f6dab0b82 100644 --- a/x-pack/test/api_integration/apis/siem/index.js +++ b/x-pack/test/api_integration/apis/siem/index.js @@ -11,6 +11,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./events')); loadTestFile(require.resolve('./hosts')); loadTestFile(require.resolve('./kpi_network')); + loadTestFile(require.resolve('./kpi_hosts')); loadTestFile(require.resolve('./network_dns')); loadTestFile(require.resolve('./network_top_n_flow')); loadTestFile(require.resolve('./overview_host')); diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts new file mode 100644 index 0000000000000..44c46d01aa0ac --- /dev/null +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -0,0 +1,424 @@ +/* + * 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 { kpiHostsQuery } from '../../../../plugins/siem/public/containers/kpi_hosts/index.gql_query'; +import { GetKpiHostsQuery } from '../../../../plugins/siem/public/graphql/types'; +import { KbnTestProvider } from './types'; + +const kpiHostsTests: KbnTestProvider = ({ getService }) => { + const esArchiver = getService('esArchiver'); + const client = getService('siemGraphQLClient'); + describe('Kpi Hosts', () => { + describe('With filebeat', () => { + before(() => esArchiver.load('filebeat/default')); + after(() => esArchiver.unload('filebeat/default')); + + const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); + const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const expectedResult = { + __typename: 'KpiHostsData', + hosts: 1, + hostsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + ], + authSuccess: 0, + authSuccessHistogram: [], + authFailure: 0, + authFailureHistogram: [], + uniqueSourceIps: 121, + uniqueSourceIpsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 52, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 31, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 88, + }, + __typename: 'HistogramData', + }, + ], + uniqueDestinationIps: 154, + uniqueDestinationIpsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 61, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 45, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 114, + }, + __typename: 'HistogramData', + }, + ], + }; + + it('Make sure that we get KpiHosts data', () => { + return client + .query({ + query: kpiHostsQuery, + variables: { + sourceId: 'default', + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + }, + }) + .then(resp => { + const kpiHosts = resp.data.source.KpiHosts; + expect(kpiHosts!).to.eql(expectedResult); + }); + }); + }); + + describe('With auditbeat', () => { + before(() => esArchiver.load('auditbeat/default')); + after(() => esArchiver.unload('auditbeat/default')); + + const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); + const TO = new Date('3000-01-01T00:00:00.000Z').valueOf(); + const expectedResult = { + __typename: 'KpiHostsData', + hosts: 1, + hostsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, + __typename: 'HistogramData', + }, + ], + authSuccess: 0, + authSuccessHistogram: [], + authFailure: 0, + authFailureHistogram: [], + uniqueSourceIps: 121, + uniqueSourceIpsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 52, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 31, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 88, + }, + __typename: 'HistogramData', + }, + ], + uniqueDestinationIps: 154, + uniqueDestinationIpsHistogram: [ + { + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 61, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 45, + }, + __typename: 'HistogramData', + }, + { + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 114, + }, + __typename: 'HistogramData', + }, + ], + }; + it('Make sure that we get KpiHosts data', () => { + return client + .query({ + query: kpiHostsQuery, + variables: { + sourceId: 'default', + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + }, + }) + .then(resp => { + const kpiHosts = resp.data.source.KpiHosts; + expect(kpiHosts!).to.eql(expectedResult); + // expect(kpiHosts!.hostsHistogram).to.eql([ + // { + // x: '2019-02-09T16:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 1, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T19:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 0, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T22:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 1, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-10T01:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 1, + // }, + // __typename: 'HistogramData', + // }, + // ]); + // expect(kpiHosts!.authSuccess).to.be(0); + // expect(kpiHosts!.authSuccessHistogram).to.eql([]); + // expect(kpiHosts!.authFailure).to.equal(0); + // expect(kpiHosts!.authFailureHistogram).to.eql([]); + // expect(kpiHosts!.uniqueSourceIps).to.equal(121); + // expect(kpiHosts!.uniqueSourceIpsHistogram).to.eql([ + // { + // x: '2019-02-09T16:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 52, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T19:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 0, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T22:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 31, + // }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-10T01:00:00.000Z', + // y: { + // __typename: 'Count', + // doc_count: null, + // value: 88, + // }, + // __typename: 'HistogramData', + // }, + // ]); + // expect(kpiHosts!.uniqueDestinationIps).to.equal(154); + // expect(kpiHosts!.uniqueDestinationIpsHistogram).to.eql([ + // { + // x: '2019-02-09T16:00:00.000Z', + // y: { value: 61, doc_count: null, __typename: 'Count' }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T19:00:00.000Z', + // y: { value: 0, doc_count: null, __typename: 'Count' }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-09T22:00:00.000Z', + // y: { value: 45, doc_count: null, __typename: 'Count' }, + // __typename: 'HistogramData', + // }, + // { + // x: '2019-02-10T01:00:00.000Z', + // y: { value: 114, doc_count: null, __typename: 'Count' }, + // __typename: 'HistogramData', + // }, + // ]); + // }); + }); + }); + }); + }); +}; +// eslint-disable-next-line import/no-default-export +export default kpiHostsTests;
+ -- + +
+ 1,714 + + Source +
+ 2,359 + + Dest. +
+ {field.value ? field.value.toLocaleString() : getEmptyTagValue()}{' '} + {field.description} +