[7.x] [SIEM] Kpi network enhancement (#36915) (#38370)

* [SIEM] Kpi network enhancement (#36915)

* add histogram

* clean up with hooks

* fix types and inport

* update mock data

* fix barchart

* fix integration test

* wrapping hooks with functional component

* update snapshot

* remove active agents and network events histogram from kpi network

* amend chart height

* fix integration test

* update wording

* add readonly type

* add references

* update translations and add constants

* clean up types for kpi host

* clean up types for kpinetwork

* remove a redundant type

* fix tests

* replace react memo with recompose pure

* add unit test and update mock data

* remove redundant type

* update translation index
This commit is contained in:
Angela Chuang 2019-06-07 17:24:52 +08:00 committed by GitHub
parent 0fdafb9c42
commit bc70842706
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1368 additions and 817 deletions

View file

@ -8,11 +8,11 @@ import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart';
import { AreaChartData } from './common';
import { ChartConfigsData } from './common';
describe('AreaChartBaseComponent', () => {
let wrapper: ReactWrapper;
const mockAreaChartData: AreaChartData[] = [
const mockAreaChartData: ChartConfigsData[] = [
{
key: 'uniqueSourceIpsHistogram',
value: [
@ -99,7 +99,7 @@ describe('AreaChartWithCustomPrompt', () => {
],
],
],
])('renders areachart', (data: AreaChartData[] | [] | null | undefined) => {
])('renders areachart', (data: ChartConfigsData[] | [] | null | undefined) => {
beforeAll(() => {
wrapper = mount(<AreaChartWithCustomPrompt height={100} width={120} data={data} />);
});
@ -181,7 +181,7 @@ describe('AreaChartWithCustomPrompt', () => {
},
],
],
])('renders prompt', (data: AreaChartData[] | [] | null | undefined) => {
])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => {
beforeAll(() => {
wrapper = mount(<AreaChartWithCustomPrompt height={100} width={120} data={data} />);
});

View file

@ -13,14 +13,16 @@ import {
getSpecId,
Position,
ScaleType,
Settings,
} from '@elastic/charts';
import '@elastic/charts/dist/style.css';
import {
AreaChartData,
ChartConfigsData,
ChartHolder,
getSeriesStyle,
numberFormatter,
WrappedByAutoSizer,
getTheme,
} from './common';
import { AutoSizer } from '../auto_sizer';
@ -28,6 +30,7 @@ const dateFormatter = (d: string) => {
return d.toLocaleString().split('T')[0];
};
// custom series styles: https://ela.st/areachart-styling
const getSeriesLineStyle = (color: string | undefined) => {
return color
? {
@ -57,14 +60,16 @@ const getSeriesLineStyle = (color: string | undefined) => {
: undefined;
};
// https://ela.st/multi-areaseries
export const AreaChartBaseComponent = React.memo<{
data: AreaChartData[];
data: ChartConfigsData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) => {
return chartConfigs.width && chartConfigs.height ? (
<div style={{ height: chartConfigs.height, width: chartConfigs.width, position: 'relative' }}>
<Chart>
<Settings theme={getTheme()} />
{data.map(series => {
const seriesKey = series.key;
const seriesSpecId = getSpecId(seriesKey);
@ -103,7 +108,7 @@ export const AreaChartBaseComponent = React.memo<{
});
export const AreaChartWithCustomPrompt = React.memo<{
data: AreaChartData[] | null | undefined;
data: ChartConfigsData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
@ -121,7 +126,7 @@ export const AreaChartWithCustomPrompt = React.memo<{
);
});
export const AreaChart = React.memo<{ areaChart: AreaChartData[] | null | undefined }>(
export const AreaChart = React.memo<{ areaChart: ChartConfigsData[] | null | undefined }>(
({ areaChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (

View file

@ -8,11 +8,11 @@ import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { BarChartBaseComponent, BarChartWithCustomPrompt } from './barchart';
import { BarChartData } from './common';
import { ChartConfigsData } from './common';
describe('BarChartBaseComponent', () => {
let wrapper: ReactWrapper;
const mockBarChartData: BarChartData[] = [
const mockBarChartData: ChartConfigsData[] = [
{
key: 'uniqueSourceIps',
value: [{ y: 1714, x: 'uniqueSourceIps', g: 'uniqueSourceIps' }],
@ -155,7 +155,7 @@ describe.each([
},
],
],
])('renders prompt', (data: BarChartData[] | [] | null | undefined) => {
])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => {
let wrapper: ReactWrapper;
beforeAll(() => {
wrapper = mount(<BarChartWithCustomPrompt height={100} width={120} data={data} />);

View file

@ -6,39 +6,22 @@
import React from 'react';
import {
Chart,
BarSeries,
Axis,
Position,
getSpecId,
ScaleType,
Settings,
mergeWithDefaultTheme,
PartialTheme,
} from '@elastic/charts';
import { Chart, BarSeries, Axis, Position, getSpecId, ScaleType, Settings } from '@elastic/charts';
import { getAxisId } from '@elastic/charts';
import {
BarChartData,
ChartConfigsData,
WrappedByAutoSizer,
ChartHolder,
numberFormatter,
SeriesType,
getSeriesStyle,
getTheme,
} from './common';
import { AutoSizer } from '../auto_sizer';
const getTheme = () => {
const theme: PartialTheme = {
scales: {
barsPadding: 0.5,
},
};
return mergeWithDefaultTheme(theme);
};
// Bar chart rotation: https://ela.st/chart-rotations
export const BarChartBaseComponent = React.memo<{
data: BarChartData[];
data: ChartConfigsData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) => {
@ -82,7 +65,7 @@ export const BarChartBaseComponent = React.memo<{
});
export const BarChartWithCustomPrompt = React.memo<{
data: BarChartData[] | null | undefined;
data: ChartConfigsData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
@ -98,7 +81,7 @@ export const BarChartWithCustomPrompt = React.memo<{
);
});
export const BarChart = React.memo<{ barChart: BarChartData[] | null | undefined }>(
export const BarChart = React.memo<{ barChart: ChartConfigsData[] | null | undefined }>(
({ barChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (

View file

@ -6,9 +6,16 @@
import { EuiFlexGroup, EuiText, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { CustomSeriesColorsMap, DataSeriesColorsValues, getSpecId } from '@elastic/charts';
import {
CustomSeriesColorsMap,
DataSeriesColorsValues,
getSpecId,
mergeWithDefaultTheme,
PartialTheme,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
const chartHeight = 74;
const FlexGroup = styled(EuiFlexGroup)`
height: 100%;
`;
@ -25,12 +32,6 @@ export const ChartHolder = () => (
</FlexGroup>
);
export interface AreaChartData {
key: string;
value: ChartData[] | [] | null;
color?: string | undefined;
}
export interface ChartData {
x: number | string | null;
y: number | string | null;
@ -38,14 +39,14 @@ export interface ChartData {
g?: number | string;
}
export interface BarChartData {
export interface ChartConfigsData {
key: string;
value: [ChartData] | [] | null;
value: ChartData[] | [] | null;
color?: string | undefined;
}
export const WrappedByAutoSizer = styled.div`
height: 100px;
height: ${chartHeight}px;
position: relative;
&:hover {
@ -63,6 +64,7 @@ export enum SeriesType {
LINE = 'line',
}
// Customize colors: https://ela.st/custom-colors
export const getSeriesStyle = (
seriesKey: string,
color: string | undefined,
@ -79,3 +81,26 @@ export const getSeriesStyle = (
return customSeriesColors;
};
// Apply margins and paddings: https://ela.st/charts-spacing
export const getTheme = () => {
const theme: PartialTheme = {
chartMargins: {
left: 0,
right: 0,
// Apply some paddings to the top to avoid chopping the y tick https://ela.st/chopping-edge
top: 4,
bottom: 0,
},
chartPaddings: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
scales: {
barsPadding: 0.5,
},
};
return mergeWithDefaultTheme(theme);
};

View file

@ -5,19 +5,20 @@
*/
import { EuiFlexGroup } from '@elastic/eui';
import { get, getOr } from 'lodash/fp';
import React from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { KpiHostsData } from '../../../../graphql/types';
import { StatItem, StatItems, StatItemsComponent, StatItemsProps } from '../../../stat_items';
import {
StatItemsComponent,
StatItemsProps,
useKpiMatrixStatus,
StatItems,
} from '../../../stat_items';
import * as i18n from './translations';
import { BarChartData, AreaChartData } from '../../../charts/common';
interface KpiHostsProps {
data: KpiHostsData;
loading: boolean;
}
const kpiWidgetHeight = 247;
const euiColorVis0 = '#00B3A4';
const euiColorVis1 = '#3185FC';
@ -25,8 +26,14 @@ const euiColorVis2 = '#DB1374';
const euiColorVis3 = '#490092';
const euiColorVis9 = '#920000';
const fieldTitleMapping: StatItems[] = [
interface KpiHostsProps {
data: KpiHostsData;
loading: boolean;
}
const fieldTitleMapping: Readonly<StatItems[]> = [
{
key: 'hosts',
fields: [
{
key: 'hosts',
@ -40,6 +47,7 @@ const fieldTitleMapping: StatItems[] = [
description: i18n.HOSTS,
},
{
key: 'authentication',
fields: [
{
key: 'authSuccess',
@ -62,6 +70,7 @@ const fieldTitleMapping: StatItems[] = [
description: i18n.AUTHENTICATION,
},
{
key: 'uniqueIps',
fields: [
{
key: 'uniqueSourceIps',
@ -86,75 +95,25 @@ const fieldTitleMapping: StatItems[] = [
},
];
export const KpiHostsComponent = React.memo<KpiHostsProps>(({ data, loading }) => {
const FlexGroupSpinner = styled(EuiFlexGroup)`
{
min-height: ${kpiWidgetHeight}px;
}
`;
export const KpiHostsComponent = ({ data, loading }: KpiHostsProps) => {
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(fieldTitleMapping, data);
return loading ? (
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ minHeight: 247 }}>
<FlexGroupSpinner justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
</FlexGroupSpinner>
) : (
<EuiFlexGroup>
{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 <StatItemsComponent {...statItemProps} />;
{statItemsProps.map(mappedStatItemProps => {
return <StatItemsComponent {...mappedStatItemProps} />;
})}
</EuiFlexGroup>
);
});
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 y: number | null = getOr(null, key, data);
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
return acc.concat([
{
...field,
value: [
{
x,
y,
g: key,
},
],
},
]);
}, []);
};

View file

@ -1,16 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KpiNetwork Component rendering it renders loading icons 1`] = `
<pure(Component)
<Component
data={
Object {
"activeAgents": 60015,
"dnsQueries": 278,
"networkEvents": 16,
"tlsHandshakes": 10000,
"uniqueDestinationPrivateIps": 18,
"uniqueDestinationPrivateIpsHistogram": Array [
Object {
"x": "2019-02-09T16:00:00.000Z",
"y": 8,
},
Object {
"x": "2019-02-09T19:00:00.000Z",
"y": 0,
},
],
"uniqueFlowId": 10277307,
"uniqueSourcePrivateIps": 383,
"uniqueSourcePrivateIpsHistogram": Array [
Object {
"x": "2019-02-09T16:00:00.000Z",
"y": 8,
},
Object {
"x": "2019-02-09T19:00:00.000Z",
"y": 0,
},
],
}
}
loading={true}
@ -18,16 +37,35 @@ exports[`KpiNetwork Component rendering it renders loading icons 1`] = `
`;
exports[`KpiNetwork Component rendering it renders the default widget 1`] = `
<pure(Component)
<Component
data={
Object {
"activeAgents": 60015,
"dnsQueries": 278,
"networkEvents": 16,
"tlsHandshakes": 10000,
"uniqueDestinationPrivateIps": 18,
"uniqueDestinationPrivateIpsHistogram": Array [
Object {
"x": "2019-02-09T16:00:00.000Z",
"y": 8,
},
Object {
"x": "2019-02-09T19:00:00.000Z",
"y": 0,
},
],
"uniqueFlowId": 10277307,
"uniqueSourcePrivateIps": 383,
"uniqueSourcePrivateIpsHistogram": Array [
Object {
"x": "2019-02-09T16:00:00.000Z",
"y": 8,
},
Object {
"x": "2019-02-09T19:00:00.000Z",
"y": 0,
},
],
}
}
loading={false}

View file

@ -5,69 +5,77 @@
*/
import { EuiFlexGroup } from '@elastic/eui';
import { get } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';
import { StatItem, StatItems, StatItemsComponent } from '../../../../components/stat_items';
import { EuiSpacer } from '@elastic/eui';
import { chunk as _chunk } from 'lodash/fp';
import {
StatItemsComponent,
StatItemsProps,
useKpiMatrixStatus,
StatItems,
} from '../../../../components/stat_items';
import { KpiNetworkData } from '../../../../graphql/types';
import * as i18n from './translations';
const kipsPerRow = 2;
const kpiWidgetHeight = 228;
const euiColorVis1 = '#3185FC';
const euiColorVis2 = '#DB1374';
const euiColorVis3 = '#490092';
interface KpiNetworkProps {
data: KpiNetworkData;
loading: boolean;
}
const fieldTitleMapping: Readonly<StatItems[]> = [
{
fields: [
{
key: 'networkEvents',
value: null,
},
],
description: i18n.NETWORK_EVENTS,
},
{
fields: [
{
key: 'uniqueFlowId',
value: null,
},
],
description: i18n.UNIQUE_ID,
},
{
fields: [
{
key: 'activeAgents',
value: null,
},
],
description: i18n.ACTIVE_AGENTS,
},
export const fieldTitleChartMapping: Readonly<StatItems[]> = [
{
key: 'UniqueIps',
fields: [
{
key: 'uniqueSourcePrivateIps',
value: null,
name: i18n.SRC,
description: i18n.SOURCE,
color: euiColorVis2,
icon: 'visMapCoordinate',
},
],
description: i18n.UNIQUE_SOURCE_PRIVATE_IPS,
},
{
fields: [
{
key: 'uniqueDestinationPrivateIps',
value: null,
name: i18n.DIST,
description: i18n.DESTINATION,
color: euiColorVis3,
icon: 'visMapCoordinate',
},
],
description: i18n.UNIQUE_DESTINATION_PRIVATE_IPS,
description: i18n.UNIQUE_PRIVATE_IPS,
enableAreaChart: true,
enableBarChart: true,
grow: 2,
},
];
const fieldTitleMatrixMapping: Readonly<StatItems[]> = [
{
key: 'networkEvents',
fields: [
{
key: 'networkEvents',
value: null,
color: euiColorVis1,
},
],
description: i18n.NETWORK_EVENTS,
grow: 1,
},
{
key: 'dnsQueries',
fields: [
{
key: 'dnsQueries',
@ -77,6 +85,17 @@ const fieldTitleMapping: Readonly<StatItems[]> = [
description: i18n.DNS_QUERIES,
},
{
key: 'uniqueFlowId',
fields: [
{
key: 'uniqueFlowId',
value: null,
},
],
description: i18n.UNIQUE_FLOW_IDS,
},
{
key: 'tlsHandshakes',
fields: [
{
key: 'tlsHandshakes',
@ -88,10 +107,28 @@ const fieldTitleMapping: Readonly<StatItems[]> = [
];
const FlexGroup = styled(EuiFlexGroup)`
margin-height: 86px;
min-height: ${kpiWidgetHeight}px;
`;
export const KpiNetworkComponent = pure<KpiNetworkProps>(({ data, loading }) => {
export const KpiNetworkBaseComponent = ({
fieldsMapping,
data,
}: {
fieldsMapping: Readonly<StatItems[]>;
data: KpiNetworkData;
}) => {
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(fieldsMapping, data);
return (
<EuiFlexGroup wrap>
{statItemsProps.map(mappedStatItemProps => {
return <StatItemsComponent {...mappedStatItemProps} />;
})}
</EuiFlexGroup>
);
};
export const KpiNetworkComponent = React.memo<KpiNetworkProps>(({ data, loading }) => {
return loading ? (
<FlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
@ -99,17 +136,18 @@ export const KpiNetworkComponent = pure<KpiNetworkProps>(({ data, loading }) =>
</EuiFlexItem>
</FlexGroup>
) : (
<EuiFlexGroup>
{fieldTitleMapping.map(stat => (
<StatItemsComponent
key={`kpi-network-summary-${stat.fields[0].key}`}
description={stat.description}
fields={addValueToFields(stat.fields, data)}
/>
))}
<EuiFlexGroup wrap>
<EuiFlexItem grow={1}>
{_chunk(kipsPerRow, fieldTitleMatrixMapping).map((mappingsPerLine, idx) => (
<React.Fragment key={`kpi-network-row-${idx}`}>
{idx % kipsPerRow === 1 && <EuiSpacer size="l" />}
<KpiNetworkBaseComponent data={data} fieldsMapping={mappingsPerLine} />
</React.Fragment>
))}
</EuiFlexItem>
<EuiFlexItem grow={1}>
<KpiNetworkBaseComponent data={data} fieldsMapping={fieldTitleChartMapping} />
</EuiFlexItem>
</EuiFlexGroup>
);
});
const addValueToFields = (fields: StatItem[], data: KpiNetworkData): StatItem[] =>
fields.map(field => ({ ...field, value: get(field.key, data) }));

View file

@ -5,15 +5,207 @@
*/
import { KpiNetworkData } from '../../../../graphql/types';
import { StatItems } from '../../../stat_items';
export const mockData: { KpiNetwork: KpiNetworkData } = {
KpiNetwork: {
networkEvents: 16,
uniqueFlowId: 10277307,
activeAgents: 60015,
uniqueSourcePrivateIps: 383,
uniqueSourcePrivateIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: 8,
},
{
x: '2019-02-09T19:00:00.000Z',
y: 0,
},
],
uniqueDestinationPrivateIps: 18,
uniqueDestinationPrivateIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: 8,
},
{
x: '2019-02-09T19:00:00.000Z',
y: 0,
},
],
dnsQueries: 278,
tlsHandshakes: 10000,
},
};
const mockMappingItems: StatItems = {
key: 'UniqueIps',
fields: [
{
key: 'uniqueSourcePrivateIps',
value: null,
name: 'Src.',
description: 'Source',
color: '#DB1374',
icon: 'visMapCoordinate',
},
{
key: 'uniqueDestinationPrivateIps',
value: null,
name: 'Dist.',
description: 'Destination',
color: '#490092',
icon: 'visMapCoordinate',
},
],
description: 'Unique Private IPs',
enableAreaChart: true,
enableBarChart: true,
grow: 2,
};
export const mockNoChartMappings: Readonly<StatItems[]> = [
{
...mockMappingItems,
enableAreaChart: false,
enableBarChart: false,
},
];
export const mockDisableChartsInitialData = {
fields: [
{
key: 'uniqueSourcePrivateIps',
value: undefined,
name: 'Src.',
description: 'Source',
color: '#DB1374',
icon: 'visMapCoordinate',
},
{
key: 'uniqueDestinationPrivateIps',
value: undefined,
name: 'Dist.',
description: 'Destination',
color: '#490092',
icon: 'visMapCoordinate',
},
],
description: 'Unique Private IPs',
enableAreaChart: false,
enableBarChart: false,
grow: 2,
areaChart: undefined,
barChart: undefined,
};
export const mockEnableChartsInitialData = {
fields: [
{
key: 'uniqueSourcePrivateIps',
value: undefined,
name: 'Src.',
description: 'Source',
color: '#DB1374',
icon: 'visMapCoordinate',
},
{
key: 'uniqueDestinationPrivateIps',
value: undefined,
name: 'Dist.',
description: 'Destination',
color: '#490092',
icon: 'visMapCoordinate',
},
],
description: 'Unique Private IPs',
enableAreaChart: true,
enableBarChart: true,
grow: 2,
areaChart: [],
barChart: [
{
color: '#DB1374',
key: 'uniqueSourcePrivateIps',
value: [
{
g: 'uniqueSourcePrivateIps',
x: 'Src.',
y: null,
},
],
},
{
color: '#490092',
key: 'uniqueDestinationPrivateIps',
value: [
{
g: 'uniqueDestinationPrivateIps',
x: 'Dist.',
y: null,
},
],
},
],
};
export const mockEnableChartsData = {
fields: [
{
key: 'uniqueSourcePrivateIps',
value: 383,
name: 'Src.',
description: 'Source',
color: '#DB1374',
icon: 'visMapCoordinate',
},
{
key: 'uniqueDestinationPrivateIps',
value: 18,
name: 'Dist.',
description: 'Destination',
color: '#490092',
icon: 'visMapCoordinate',
},
],
description: 'Unique Private IPs',
enableAreaChart: true,
enableBarChart: true,
grow: 2,
areaChart: [
{
key: 'uniqueSourcePrivateIpsHistogram',
value: [
{ x: '2019-02-09T16:00:00.000Z', y: 8 },
{
x: '2019-02-09T19:00:00.000Z',
y: 0,
},
],
name: 'Src.',
description: 'Source',
color: '#DB1374',
icon: 'visMapCoordinate',
},
{
key: 'uniqueDestinationPrivateIpsHistogram',
value: [{ x: '2019-02-09T16:00:00.000Z', y: 8 }, { x: '2019-02-09T19:00:00.000Z', y: 0 }],
name: 'Dist.',
description: 'Destination',
color: '#490092',
icon: 'visMapCoordinate',
},
],
barChart: [
{
key: 'uniqueSourcePrivateIps',
color: '#DB1374',
value: [{ x: 'Src.', y: 383, g: 'uniqueSourcePrivateIps' }],
},
{
key: 'uniqueDestinationPrivateIps',
color: '#490092',
value: [{ x: 'Dist.', y: 18, g: 'uniqueDestinationPrivateIps' }],
},
],
};

View file

@ -10,27 +10,36 @@ export const NETWORK_EVENTS = i18n.translate('xpack.siem.kpiNetwork.source.netwo
defaultMessage: 'Network Events',
});
export const UNIQUE_ID = i18n.translate('xpack.siem.kpiNetwork.source.uniquiIdTitle', {
defaultMessage: 'Unique Flow ID',
export const UNIQUE_FLOW_IDS = i18n.translate('xpack.siem.kpiNetwork.source.uniqueFlowIdsTitle', {
defaultMessage: 'Unique Flow IDs',
});
export const ACTIVE_AGENTS = i18n.translate('xpack.siem.kpiNetwork.source.activeAgentsTitle', {
defaultMessage: 'Active Agents',
});
export const UNIQUE_SOURCE_PRIVATE_IPS = i18n.translate(
'xpack.siem.kpiNetwork.source.uniqueSourcePrivateIpsTitle',
export const UNIQUE_PRIVATE_IPS = i18n.translate(
'xpack.siem.kpiNetwork.source.uniquePrivateIpsTitle',
{
defaultMessage: 'Unique Source IPs',
defaultMessage: 'Unique Private IPs',
}
);
export const UNIQUE_DESTINATION_PRIVATE_IPS = i18n.translate(
'xpack.siem.kpiNetwork.source.uniqueDestinationPrivateIpsTitle',
{
defaultMessage: 'Unique Destination IPs',
}
);
export const SRC = i18n.translate('xpack.siem.kpiNetwork.source.srcTitle', {
defaultMessage: 'Src.',
});
export const SOURCE = i18n.translate('xpack.siem.kpiNetwork.source.sourceTitle', {
defaultMessage: 'Source',
});
export const DESTINATION = i18n.translate('xpack.siem.kpiNetwork.source.destinationTitle', {
defaultMessage: 'Destination',
});
export const DIST = i18n.translate('xpack.siem.kpiNetwork.source.distTitle', {
defaultMessage: 'Dist.',
});
export const LOADING = i18n.translate('xpack.siem.kpiNetwork.source.loadingDescription', {
defaultMessage: 'Loading',

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Stat Items disable charts it renders the default widget 1`] = `
exports[`Stat Items Component disable charts it renders the default widget 1`] = `
<Component
description="HOSTS"
fields={
@ -102,7 +102,7 @@ exports[`Stat Items disable charts it renders the default widget 1`] = `
</Component>
`;
exports[`Stat Items disable charts it renders the default widget 2`] = `
exports[`Stat Items Component disable charts it renders the default widget 2`] = `
<Component
areaChart={Array []}
barChart={Array []}
@ -207,7 +207,7 @@ exports[`Stat Items disable charts it renders the default widget 2`] = `
</Component>
`;
exports[`Stat Items rendering kpis with charts it renders the default widget 1`] = `
exports[`Stat Items Component rendering kpis with charts it renders the default widget 1`] = `
<Component
areaChart={
Array [
@ -256,8 +256,8 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueSourceIps",
"value": Array [
Object {
"x": 1714,
"y": "uniqueSourceIps",
"x": "uniqueSourceIps",
"y": "1714",
},
],
},
@ -266,8 +266,8 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueDestinationIps",
"value": Array [
Object {
"x": 2354,
"y": "uniqueDestinationIps",
"x": "uniqueDestinationIps",
"y": 2354,
},
],
},
@ -531,8 +531,8 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueSourceIps",
"value": Array [
Object {
"x": 1714,
"y": "uniqueSourceIps",
"x": "uniqueSourceIps",
"y": "1714",
},
],
},
@ -541,8 +541,8 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueDestinationIps",
"value": Array [
Object {
"x": 2354,
"y": "uniqueDestinationIps",
"x": "uniqueDestinationIps",
"y": 2354,
},
],
},
@ -557,7 +557,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
innerRef={[Function]}
>
<div
className="sc-bwzfXH gluXfG"
className="sc-bwzfXH ffMqh"
>
<Component
data={
@ -567,8 +567,8 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueSourceIps",
"value": Array [
Object {
"x": 1714,
"y": "uniqueSourceIps",
"x": "uniqueSourceIps",
"y": "1714",
},
],
},
@ -577,67 +577,40 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
"key": "uniqueDestinationIps",
"value": Array [
Object {
"x": 2354,
"y": "uniqueDestinationIps",
"x": "uniqueDestinationIps",
"y": 2354,
},
],
},
]
}
>
<ChartHolder>
<Styled(EuiFlexGroup)
alignItems="center"
justifyContent="center"
>
<EuiFlexGroup
alignItems="center"
className="sc-bdVaJa dGIfxd"
justifyContent="center"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive sc-bdVaJa dGIfxd"
>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<EuiText
color="subdued"
size="s"
textAlign="center"
>
<div
className="euiText euiText--small"
>
<EuiTextAlign
textAlign="center"
>
<div
className="euiTextAlign euiTextAlign--center"
>
<EuiTextColor
color="subdued"
component="div"
>
<div
className="euiTextColor euiTextColor--subdued"
>
Chart Data Not Available
</div>
</EuiTextColor>
</div>
</EuiTextAlign>
</div>
</EuiText>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</Styled(EuiFlexGroup)>
</ChartHolder>
<Component
data={
Array [
Object {
"color": "#DB1374",
"key": "uniqueSourceIps",
"value": Array [
Object {
"x": "uniqueSourceIps",
"y": "1714",
},
],
},
Object {
"color": "#490092",
"key": "uniqueDestinationIps",
"value": Array [
Object {
"x": "uniqueDestinationIps",
"y": 2354,
},
],
},
]
}
/>
</Component>
</div>
</styled.div>
@ -703,7 +676,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`]
innerRef={[Function]}
>
<div
className="sc-bwzfXH gluXfG"
className="sc-bwzfXH ffMqh"
>
<Component
data={

View file

@ -8,12 +8,26 @@ import { mount, ReactWrapper } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { StatItemsComponent, StatItemsProps } from '.';
import {
StatItemsComponent,
StatItemsProps,
addValueToFields,
addValueToAreaChart,
addValueToBarChart,
} from '.';
import { BarChart } from '../charts/barchart';
import { AreaChart } from '../charts/areachart';
import { EuiHorizontalRule } from '@elastic/eui';
import { fieldTitleChartMapping, KpiNetworkBaseComponent } from '../page/network/kpi_network';
import {
mockData,
mockNoChartMappings,
mockDisableChartsInitialData,
mockEnableChartsData,
mockEnableChartsInitialData,
} from '../page/network/kpi_network/mock';
describe('Stat Items', () => {
describe('Stat Items Component', () => {
describe.each([
[
mount(
@ -102,10 +116,10 @@ describe('Stat Items', () => {
},
],
barChart: [
{ key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 2354, y: 'uniqueDestinationIps' }],
value: [{ x: 'uniqueDestinationIps', y: 2354 }],
color: '#490092',
},
],
@ -141,3 +155,72 @@ describe('Stat Items', () => {
});
});
});
describe('addValueToFields', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
const mockKpiNetworkData = mockData.KpiNetwork;
test('should update value from data', () => {
const result = addValueToFields(mockNetworkMappings.fields, mockKpiNetworkData);
expect(result).toEqual(mockEnableChartsData.fields);
});
});
describe('addValueToAreaChart', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
const mockKpiNetworkData = mockData.KpiNetwork;
test('should add areaChart from data', () => {
const result = addValueToAreaChart(mockNetworkMappings.fields, mockKpiNetworkData);
expect(result).toEqual(mockEnableChartsData.areaChart);
});
});
describe('addValueToBarChart', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
const mockKpiNetworkData = mockData.KpiNetwork;
test('should add areaChart from data', () => {
const result = addValueToBarChart(mockNetworkMappings.fields, mockKpiNetworkData);
expect(result).toEqual(mockEnableChartsData.barChart);
});
});
describe('useKpiMatrixStatus', () => {
const mockNetworkMappings = fieldTitleChartMapping;
const mockKpiNetworkData = mockData.KpiNetwork;
test('it updates status correctly', () => {
const wrapper = mount(
<KpiNetworkBaseComponent fieldsMapping={mockNetworkMappings} data={{}} />
);
expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockEnableChartsInitialData);
wrapper.setProps({ data: mockKpiNetworkData });
wrapper.update();
expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockEnableChartsData);
});
test('it should not append areaChart if enableAreaChart is off', () => {
const mockNetworkMappingsNoAreaChart = mockNoChartMappings;
const wrapper = mount(
<KpiNetworkBaseComponent fieldsMapping={mockNetworkMappingsNoAreaChart} data={{}} />
);
expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockDisableChartsInitialData);
wrapper.setProps({ data: mockKpiNetworkData });
wrapper.update();
expect(wrapper.find(StatItemsComponent).get(0).props.areaChart).toBeUndefined();
});
test('it should not append barChart if enableBarChart is off', () => {
const mockNetworkMappingsNoAreaChart = mockNoChartMappings;
const wrapper = mount(
<KpiNetworkBaseComponent fieldsMapping={mockNetworkMappingsNoAreaChart} data={{}} />
);
expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockDisableChartsInitialData);
wrapper.setProps({ data: mockKpiNetworkData });
wrapper.update();
expect(wrapper.find(StatItemsComponent).get(0).props.barChart).toBeUndefined();
});
});

View file

@ -13,13 +13,15 @@ import {
EuiTitle,
IconType,
} from '@elastic/eui';
import React from 'react';
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { get, getOr } from 'lodash/fp';
import { BarChart } from '../charts/barchart';
import { AreaChart } from '../charts/areachart';
import { getEmptyTagValue } from '../empty_value';
import { AreaChartData, BarChartData } from '../charts/common';
import { ChartConfigsData, ChartData } from '../charts/common';
import { KpiHostsData, KpiNetworkData } from '../../graphql/types';
const FlexItem = styled(EuiFlexItem)`
min-width: 0;
@ -31,7 +33,7 @@ const StatValue = styled(EuiTitle)`
white-space: nowrap;
`;
export interface StatItem {
interface StatItem {
key: string;
description?: string;
value: number | undefined | null;
@ -41,6 +43,7 @@ export interface StatItem {
}
export interface StatItems {
key: string;
fields: StatItem[];
description?: string;
enableAreaChart?: boolean;
@ -49,11 +52,81 @@ export interface StatItems {
}
export interface StatItemsProps extends StatItems {
key: string;
areaChart?: AreaChartData[];
barChart?: BarChartData[];
areaChart?: ChartConfigsData[];
barChart?: ChartConfigsData[];
}
export const addValueToFields = (
fields: StatItem[],
data: KpiHostsData | KpiNetworkData
): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) }));
export const addValueToAreaChart = (
fields: StatItem[],
data: KpiHostsData | KpiNetworkData
): ChartConfigsData[] =>
fields
.filter(field => get(`${field.key}Histogram`, data) != null)
.map(field => ({
...field,
value: get(`${field.key}Histogram`, data),
key: `${field.key}Histogram`,
}));
export const addValueToBarChart = (
fields: StatItem[],
data: KpiHostsData | KpiNetworkData
): ChartConfigsData[] => {
if (fields.length === 0) return [];
return fields.reduce((acc: ChartConfigsData[], field: StatItem, idx: number) => {
const { key, color } = field;
const y: number | null = getOr(null, key, data);
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
const value: [ChartData] = [
{
x,
y,
g: key,
},
];
return [
...acc,
{
key,
color,
value,
},
];
}, []);
};
export const useKpiMatrixStatus = (
mappings: Readonly<StatItems[]>,
data: KpiHostsData | KpiNetworkData
): StatItemsProps[] => {
const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]);
useEffect(
() => {
setStatItemsProps(
mappings.map(stat => {
return {
...stat,
key: `kpi-summary-${stat.key}`,
fields: addValueToFields(stat.fields, data),
areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined,
barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined,
};
})
);
},
[data]
);
return statItemsProps;
};
export const StatItemsComponent = React.memo<StatItemsProps>(
({ fields, description, grow, barChart, areaChart, enableAreaChart, enableBarChart }) => {
const isBarChartDataAbailable =

View file

@ -7,12 +7,9 @@
import gql from 'graphql-tag';
export const kpiHostsQuery = gql`
fragment ChartFields on KpiHostHistogramData {
x: key_as_string
y: count {
value
doc_count
}
fragment KpiHostChartFields on KpiHostHistogramData {
x
y
}
query GetKpiHostsQuery(
@ -26,23 +23,23 @@ export const kpiHostsQuery = gql`
KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {
hosts
hostsHistogram {
...ChartFields
...KpiHostChartFields
}
authSuccess
authSuccessHistogram {
...ChartFields
...KpiHostChartFields
}
authFailure
authFailureHistogram {
...ChartFields
...KpiHostChartFields
}
uniqueSourceIps
uniqueSourceIpsHistogram {
...ChartFields
...KpiHostChartFields
}
uniqueDestinationIps
uniqueDestinationIpsHistogram {
...ChartFields
...KpiHostChartFields
}
}
}

View file

@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr, get } from 'lodash/fp';
import { getOr } from 'lodash/fp';
import React from 'react';
import { Query } from 'react-apollo';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
@ -17,7 +16,6 @@ import { createFilter } from '../helpers';
import { QueryTemplateProps } from '../query_template';
import { kpiHostsQuery } from './index.gql_query';
import { ChartData } from '../../components/charts/common';
export interface KpiHostsArgs {
id: string;
@ -30,21 +28,7 @@ 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.length > 0
? data.map(({ x, y }) => ({
x,
y: y.value || y.doc_count,
}))
: [];
};
export const KpiHostsQuery = pure<KpiHostsProps>(
export const KpiHostsQuery = React.memo<KpiHostsProps>(
({ id = 'kpiHostsQuery', children, filterQuery, sourceId, startDate, endDate }) => (
<Query<GetKpiHostsQuery.Query, GetKpiHostsQuery.Variables>
query={kpiHostsQuery}
@ -63,29 +47,9 @@ export const KpiHostsQuery = pure<KpiHostsProps>(
>
{({ 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)
: [],
},
kpiHosts,
loading,
refetch,
});

View file

@ -7,6 +7,11 @@
import gql from 'graphql-tag';
export const kpiNetworkQuery = gql`
fragment KpiNetworkChartFields on KpiNetworkHistogramData {
x
y
}
query GetKpiNetworkQuery(
$sourceId: ID!
$timerange: TimerangeInput!
@ -18,9 +23,14 @@ export const kpiNetworkQuery = gql`
KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {
networkEvents
uniqueFlowId
activeAgents
uniqueSourcePrivateIps
uniqueSourcePrivateIpsHistogram {
...KpiNetworkChartFields
}
uniqueDestinationPrivateIps
uniqueDestinationPrivateIpsHistogram {
...KpiNetworkChartFields
}
dnsQueries
tlsHandshakes
}

View file

@ -7,7 +7,6 @@
import { getOr } from 'lodash/fp';
import React from 'react';
import { Query } from 'react-apollo';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
@ -29,7 +28,7 @@ export interface KpiNetworkProps extends QueryTemplateProps {
children: (args: KpiNetworkArgs) => React.ReactNode;
}
export const KpiNetworkQuery = pure<KpiNetworkProps>(
export const KpiNetworkQuery = React.memo<KpiNetworkProps>(
({ id = 'kpiNetworkQuery', children, filterQuery, sourceId, startDate, endDate }) => (
<Query<GetKpiNetworkQuery.Query, GetKpiNetworkQuery.Variables>
query={kpiNetworkQuery}

View file

@ -6853,14 +6853,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "activeAgents",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "uniqueSourcePrivateIps",
"description": "",
@ -6869,6 +6861,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "uniqueSourcePrivateIpsHistogram",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "KpiNetworkHistogramData", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "uniqueDestinationPrivateIps",
"description": "",
@ -6877,6 +6885,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "uniqueDestinationPrivateIpsHistogram",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "KpiNetworkHistogramData", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dnsQueries",
"description": "",
@ -6899,6 +6923,33 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "KpiNetworkHistogramData",
"description": "",
"fields": [
{
"name": "x",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "y",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "KpiHostsData",
@ -7036,15 +7087,7 @@
"description": "",
"fields": [
{
"name": "key",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "key_as_string",
"name": "x",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
@ -7052,34 +7095,7 @@
"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",
"name": "y",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },

View file

@ -1054,17 +1054,25 @@ export interface KpiNetworkData {
uniqueFlowId?: number | null;
activeAgents?: number | null;
uniqueSourcePrivateIps?: number | null;
uniqueSourcePrivateIpsHistogram?: KpiNetworkHistogramData[] | null;
uniqueDestinationPrivateIps?: number | null;
uniqueDestinationPrivateIpsHistogram?: KpiNetworkHistogramData[] | null;
dnsQueries?: number | null;
tlsHandshakes?: number | null;
}
export interface KpiNetworkHistogramData {
x?: string | null;
y?: number | null;
}
export interface KpiHostsData {
hosts?: number | null;
@ -1088,17 +1096,9 @@ export interface KpiHostsData {
}
export interface KpiHostHistogramData {
key?: number | null;
x?: string | null;
key_as_string?: string | null;
count?: Count | null;
}
export interface Count {
value?: number | null;
doc_count?: number | null;
y?: number | null;
}
export interface NetworkTopNFlowData {
@ -2956,15 +2956,15 @@ export namespace GetKpiHostsQuery {
uniqueDestinationIpsHistogram?: UniqueDestinationIpsHistogram[] | null;
};
export type HostsHistogram = ChartFields.Fragment;
export type HostsHistogram = KpiHostChartFields.Fragment;
export type AuthSuccessHistogram = ChartFields.Fragment;
export type AuthSuccessHistogram = KpiHostChartFields.Fragment;
export type AuthFailureHistogram = ChartFields.Fragment;
export type AuthFailureHistogram = KpiHostChartFields.Fragment;
export type UniqueSourceIpsHistogram = ChartFields.Fragment;
export type UniqueSourceIpsHistogram = KpiHostChartFields.Fragment;
export type UniqueDestinationIpsHistogram = ChartFields.Fragment;
export type UniqueDestinationIpsHistogram = KpiHostChartFields.Fragment;
}
export namespace GetKpiNetworkQuery {
@ -2996,16 +2996,22 @@ export namespace GetKpiNetworkQuery {
uniqueFlowId?: number | null;
activeAgents?: number | null;
uniqueSourcePrivateIps?: number | null;
uniqueSourcePrivateIpsHistogram?: UniqueSourcePrivateIpsHistogram[] | null;
uniqueDestinationPrivateIps?: number | null;
uniqueDestinationPrivateIpsHistogram?: UniqueDestinationPrivateIpsHistogram[] | null;
dnsQueries?: number | null;
tlsHandshakes?: number | null;
};
export type UniqueSourcePrivateIpsHistogram = KpiNetworkChartFields.Fragment;
export type UniqueDestinationPrivateIpsHistogram = KpiNetworkChartFields.Fragment;
}
export namespace GetNetworkDnsQuery {
@ -5031,20 +5037,22 @@ export namespace GetUsersQuery {
};
}
export namespace ChartFields {
export namespace KpiHostChartFields {
export type Fragment = {
__typename?: 'KpiHostHistogramData';
x?: string | null;
y?: Y | null;
};
export type Y = {
__typename?: 'Count';
value?: number | null;
doc_count?: number | null;
y?: number | null;
};
}
export namespace KpiNetworkChartFields {
export type Fragment = {
__typename?: 'KpiNetworkHistogramData';
x?: string | null;
y?: number | null;
};
}

View file

@ -7,15 +7,9 @@
import gql from 'graphql-tag';
export const kpiHostsSchema = gql`
type Count {
value: Float
doc_count: Float
}
type KpiHostHistogramData {
key: Float
key_as_string: String
count: Count
x: String
y: Float
}
type KpiHostsData {

View file

@ -7,12 +7,18 @@
import gql from 'graphql-tag';
export const kpiNetworkSchema = gql`
type KpiNetworkHistogramData {
x: String
y: Float
}
type KpiNetworkData {
networkEvents: Float
uniqueFlowId: Float
activeAgents: Float
uniqueSourcePrivateIps: Float
uniqueSourcePrivateIpsHistogram: [KpiNetworkHistogramData!]
uniqueDestinationPrivateIps: Float
uniqueDestinationPrivateIpsHistogram: [KpiNetworkHistogramData!]
dnsQueries: Float
tlsHandshakes: Float
}

View file

@ -1083,17 +1083,25 @@ export interface KpiNetworkData {
uniqueFlowId?: number | null;
activeAgents?: number | null;
uniqueSourcePrivateIps?: number | null;
uniqueSourcePrivateIpsHistogram?: KpiNetworkHistogramData[] | null;
uniqueDestinationPrivateIps?: number | null;
uniqueDestinationPrivateIpsHistogram?: KpiNetworkHistogramData[] | null;
dnsQueries?: number | null;
tlsHandshakes?: number | null;
}
export interface KpiNetworkHistogramData {
x?: string | null;
y?: number | null;
}
export interface KpiHostsData {
hosts?: number | null;
@ -1117,17 +1125,9 @@ export interface KpiHostsData {
}
export interface KpiHostHistogramData {
key?: number | null;
x?: string | null;
key_as_string?: string | null;
count?: Count | null;
}
export interface Count {
value?: number | null;
doc_count?: number | null;
y?: number | null;
}
export interface NetworkTopNFlowData {
@ -5805,16 +5805,26 @@ export namespace KpiNetworkDataResolvers {
uniqueFlowId?: UniqueFlowIdResolver<number | null, TypeParent, Context>;
activeAgents?: ActiveAgentsResolver<number | null, TypeParent, Context>;
uniqueSourcePrivateIps?: UniqueSourcePrivateIpsResolver<number | null, TypeParent, Context>;
uniqueSourcePrivateIpsHistogram?: UniqueSourcePrivateIpsHistogramResolver<
KpiNetworkHistogramData[] | null,
TypeParent,
Context
>;
uniqueDestinationPrivateIps?: UniqueDestinationPrivateIpsResolver<
number | null,
TypeParent,
Context
>;
uniqueDestinationPrivateIpsHistogram?: UniqueDestinationPrivateIpsHistogramResolver<
KpiNetworkHistogramData[] | null,
TypeParent,
Context
>;
dnsQueries?: DnsQueriesResolver<number | null, TypeParent, Context>;
tlsHandshakes?: TlsHandshakesResolver<number | null, TypeParent, Context>;
@ -5830,21 +5840,26 @@ export namespace KpiNetworkDataResolvers {
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type ActiveAgentsResolver<
R = number | null,
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type UniqueSourcePrivateIpsResolver<
R = number | null,
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type UniqueSourcePrivateIpsHistogramResolver<
R = KpiNetworkHistogramData[] | null,
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type UniqueDestinationPrivateIpsResolver<
R = number | null,
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type UniqueDestinationPrivateIpsHistogramResolver<
R = KpiNetworkHistogramData[] | null,
Parent = KpiNetworkData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DnsQueriesResolver<
R = number | null,
Parent = KpiNetworkData,
@ -5857,6 +5872,25 @@ export namespace KpiNetworkDataResolvers {
> = Resolver<R, Parent, Context>;
}
export namespace KpiNetworkHistogramDataResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = KpiNetworkHistogramData> {
x?: XResolver<string | null, TypeParent, Context>;
y?: YResolver<number | null, TypeParent, Context>;
}
export type XResolver<
R = string | null,
Parent = KpiNetworkHistogramData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type YResolver<
R = number | null,
Parent = KpiNetworkHistogramData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace KpiHostsDataResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = KpiHostsData> {
hosts?: HostsResolver<number | null, TypeParent, Context>;
@ -5950,49 +5984,23 @@ export namespace KpiHostsDataResolvers {
export namespace KpiHostHistogramDataResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = KpiHostHistogramData> {
key?: KeyResolver<number | null, TypeParent, Context>;
x?: XResolver<string | null, TypeParent, Context>;
key_as_string?: KeyAsStringResolver<string | null, TypeParent, Context>;
count?: CountResolver<Count | null, TypeParent, Context>;
y?: YResolver<number | null, TypeParent, Context>;
}
export type KeyResolver<
R = number | null,
Parent = KpiHostHistogramData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type KeyAsStringResolver<
export type XResolver<
R = string | null,
Parent = KpiHostHistogramData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type CountResolver<
R = Count | null,
export type YResolver<
R = number | null,
Parent = KpiHostHistogramData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace CountResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = Count> {
value?: ValueResolver<number | null, TypeParent, Context>;
doc_count?: DocCountResolver<number | null, TypeParent, Context>;
}
export type ValueResolver<R = number | null, Parent = Count, Context = SiemContext> = Resolver<
R,
Parent,
Context
>;
export type DocCountResolver<R = number | null, Parent = Count, Context = SiemContext> = Resolver<
R,
Parent,
Context
>;
}
export namespace NetworkTopNFlowDataResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = NetworkTopNFlowData> {
edges?: EdgesResolver<NetworkTopNFlowEdges[], TypeParent, Context>;

View file

@ -4,7 +4,6 @@
* 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';
@ -19,6 +18,7 @@ import {
} from './mock';
import * as authQueryDsl from './query_authentication.dsl';
import * as generalQueryDsl from './query_general.dsl';
import { KpiHostsMappedData } from './types';
describe('Hosts Kpi elasticsearch_adapter', () => {
const mockCallWithRequest = jest.fn();
@ -33,7 +33,7 @@ describe('Hosts Kpi elasticsearch_adapter', () => {
let mockBuildQuery: jest.SpyInstance;
let mockBuildAuthQuery: jest.SpyInstance;
let EsKpiHosts: ElasticsearchKpiHostsAdapter;
let data: KpiHostsData;
let data: KpiHostsMappedData;
describe('getKpiHosts - call stack', () => {
beforeAll(async () => {

View file

@ -6,7 +6,6 @@
import { getOr } from 'lodash/fp';
import { KpiHostsData } from '../../graphql/types';
import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework';
import { TermAggregation } from '../types';
@ -17,7 +16,34 @@ import {
KpiHostsESMSearchBody,
KpiHostsGeneralHit,
KpiHostsAuthHit,
KpiHostsMappedData,
KpiHostHistogram,
KpiHostGeneralHistogramCount,
KpiHostAuthHistogramCount,
} from './types';
import { KpiHostHistogramData } from '../../graphql/types';
const formatGeneralHistogramData = (
data: Array<KpiHostHistogram<KpiHostGeneralHistogramCount>>
): KpiHostHistogramData[] | null => {
return data && data.length > 0
? data.map<KpiHostHistogramData>(({ key_as_string, count }) => ({
x: key_as_string,
y: count.value,
}))
: null;
};
const formatAuthHistogramData = (
data: Array<KpiHostHistogram<KpiHostAuthHistogramCount>>
): KpiHostHistogramData[] | null => {
return data && data.length > 0
? data.map<KpiHostHistogramData>(({ key_as_string, count }) => ({
x: key_as_string,
y: count.doc_count,
}))
: null;
};
export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
@ -25,7 +51,7 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter {
public async getKpiHosts(
request: FrameworkRequest,
options: RequestBasicOptions
): Promise<KpiHostsData> {
): Promise<KpiHostsMappedData> {
const generalQuery: KpiHostsESMSearchBody[] = buildGeneralQuery(options);
const authQuery: KpiHostsESMSearchBody[] = buildAuthQuery(options);
const response = await this.framework.callWithRequest<
@ -34,45 +60,55 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter {
>(request, 'msearch', {
body: [...generalQuery, ...authQuery],
});
const hostsHistogram = getOr(
null,
'responses.0.aggregations.hosts_histogram.buckets',
response
);
const authSuccessHistogram = getOr(
null,
'responses.1.aggregations.authentication_success_histogram.buckets',
response
);
const authFailureHistogram = getOr(
null,
'responses.1.aggregations.authentication_failure_histogram.buckets',
response
);
const uniqueSourceIpsHistogram = getOr(
null,
'responses.0.aggregations.unique_source_ips_histogram.buckets',
response
);
const uniqueDestinationIpsHistogram = getOr(
null,
'responses.0.aggregations.unique_destination_ips_histogram.buckets',
response
);
return {
hosts: getOr(null, 'responses.0.aggregations.hosts.value', response),
hostsHistogram: getOr(null, 'responses.0.aggregations.hosts_histogram.buckets', response),
hostsHistogram: formatGeneralHistogramData(hostsHistogram),
authSuccess: getOr(
null,
'responses.1.aggregations.authentication_success.doc_count',
response
),
authSuccessHistogram: getOr(
null,
'responses.1.aggregations.authentication_success_histogram.buckets',
response
),
authSuccessHistogram: formatAuthHistogramData(authSuccessHistogram),
authFailure: getOr(
null,
'responses.1.aggregations.authentication_failure.doc_count',
response
),
authFailureHistogram: getOr(
null,
'responses.1.aggregations.authentication_failure_histogram.buckets',
response
),
authFailureHistogram: formatAuthHistogramData(authFailureHistogram),
uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response),
uniqueSourceIpsHistogram: getOr(
null,
'responses.0.aggregations.unique_source_ips_histogram.buckets',
response
),
uniqueSourceIpsHistogram: formatGeneralHistogramData(uniqueSourceIpsHistogram),
uniqueDestinationIps: getOr(
null,
'responses.0.aggregations.unique_destination_ips.value',
response
),
uniqueDestinationIpsHistogram: getOr(
null,
'responses.0.aggregations.unique_destination_ips_histogram.buckets',
response
),
uniqueDestinationIpsHistogram: formatGeneralHistogramData(uniqueDestinationIpsHistogram),
};
}
}

View file

@ -4,10 +4,9 @@
* 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';
import { KpiHostsAdapter, KpiHostsMappedData } from './types';
export class KpiHosts {
constructor(private readonly adapter: KpiHostsAdapter) {}
@ -15,7 +14,7 @@ export class KpiHosts {
public async getKpiHosts(
req: FrameworkRequest,
options: RequestBasicOptions
): Promise<KpiHostsData> {
): Promise<KpiHostsMappedData> {
return await this.adapter.getKpiHosts(req, options);
}
}

View file

@ -241,136 +241,76 @@ export const mockResult = {
hosts: 986,
hostsHistogram: [
{
key_as_string: '2019-05-03T13:00:00.000Z',
key: 1556888400000,
doc_count: 3158515,
count: {
value: 919,
},
x: '2019-05-03T13:00:00.000Z',
y: 919,
},
{
key_as_string: '2019-05-04T01:00:00.000Z',
key: 1556931600000,
doc_count: 703032,
count: {
value: 82,
},
x: '2019-05-04T01:00:00.000Z',
y: 82,
},
{
key_as_string: '2019-05-04T13:00:00.000Z',
key: 1556974800000,
doc_count: 1780,
count: {
value: 4,
},
x: '2019-05-04T13:00:00.000Z',
y: 4,
},
],
authSuccess: 61,
authSuccessHistogram: [
{
key_as_string: '2019-05-03T13:00:00.000Z',
key: 1556888400000,
doc_count: 11739,
count: {
doc_count: 8,
},
x: '2019-05-03T13:00:00.000Z',
y: 8,
},
{
key_as_string: '2019-05-04T01:00:00.000Z',
key: 1556931600000,
doc_count: 4031,
count: {
doc_count: 52,
},
x: '2019-05-04T01:00:00.000Z',
y: 52,
},
{
key_as_string: '2019-05-04T13:00:00.000Z',
key: 1556974800000,
doc_count: 13,
count: {
doc_count: 1,
},
x: '2019-05-04T13:00:00.000Z',
y: 1,
},
],
authFailure: 15722,
authFailureHistogram: [
{
key_as_string: '2019-05-03T13:00:00.000Z',
key: 1556888400000,
doc_count: 11739,
count: {
doc_count: 11731,
},
x: '2019-05-03T13:00:00.000Z',
y: 11731,
},
{
key_as_string: '2019-05-04T01:00:00.000Z',
key: 1556931600000,
doc_count: 4031,
count: {
doc_count: 3979,
},
x: '2019-05-04T01:00:00.000Z',
y: 3979,
},
{
key_as_string: '2019-05-04T13:00:00.000Z',
key: 1556974800000,
doc_count: 13,
count: {
doc_count: 12,
},
x: '2019-05-04T13:00:00.000Z',
y: 12,
},
],
uniqueSourceIps: 1407,
uniqueSourceIpsHistogram: [
{
key_as_string: '2019-05-03T13:00:00.000Z',
key: 1556888400000,
doc_count: 3158515,
count: {
value: 1182,
},
x: '2019-05-03T13:00:00.000Z',
y: 1182,
},
{
key_as_string: '2019-05-04T01:00:00.000Z',
key: 1556931600000,
doc_count: 703032,
count: {
value: 364,
},
x: '2019-05-04T01:00:00.000Z',
y: 364,
},
{
key_as_string: '2019-05-04T13:00:00.000Z',
key: 1556974800000,
doc_count: 1780,
count: {
value: 63,
},
x: '2019-05-04T13:00:00.000Z',
y: 63,
},
],
uniqueDestinationIps: 1954,
uniqueDestinationIpsHistogram: [
{
key_as_string: '2019-05-03T13:00:00.000Z',
key: 1556888400000,
doc_count: 3158515,
count: {
value: 1809,
},
x: '2019-05-03T13:00:00.000Z',
y: 1809,
},
{
key_as_string: '2019-05-04T01:00:00.000Z',
key: 1556931600000,
doc_count: 703032,
count: {
value: 407,
},
x: '2019-05-04T01:00:00.000Z',
y: 407,
},
{
key_as_string: '2019-05-04T13:00:00.000Z',
key: 1556974800000,
doc_count: 1780,
count: {
value: 64,
},
x: '2019-05-04T13:00:00.000Z',
y: 64,
},
],
};

View file

@ -3,12 +3,40 @@
* 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';
import { KpiHostHistogramData } from '../../graphql/types';
export interface KpiHostsMappedData {
hosts?: number | null;
hostsHistogram?: KpiHostHistogramData[] | null;
authSuccess?: number | null;
authSuccessHistogram?: KpiHostHistogramData[] | null;
authFailure?: number | null;
authFailureHistogram?: KpiHostHistogramData[] | null;
uniqueSourceIps?: number | null;
uniqueSourceIpsHistogram?: KpiHostHistogramData[] | null;
uniqueDestinationIps?: number | null;
uniqueDestinationIpsHistogram?: KpiHostHistogramData[] | null;
}
export interface KpiHostsAdapter {
getKpiHosts(request: FrameworkRequest, options: RequestBasicOptions): Promise<KpiHostsData>;
getKpiHosts(request: FrameworkRequest, options: RequestBasicOptions): Promise<KpiHostsMappedData>;
}
export interface KpiHostHistogram<T> {
key_as_string: string;
key: number;
doc_count: number;
count: T;
}
export interface KpiHostGeneralHistogramCount {
value: number;
}
export interface KpiHostAuthHistogramCount {
doc_count: number;
}
export interface KpiHostsGeneralHit extends SearchHit {
@ -17,46 +45,19 @@ export interface KpiHostsGeneralHit extends SearchHit {
value: number;
};
hosts_histogram: {
buckets: [
{
key_as_string: string;
key: number;
doc_count: number;
count: {
value: number;
};
}
];
buckets: Array<KpiHostHistogram<KpiHostGeneralHistogramCount>>;
};
unique_source_ips: {
value: number;
};
unique_source_ips_histogram: {
buckets: [
{
key_as_string: string;
key: number;
doc_count: number;
count: {
value: number;
};
}
];
buckets: Array<KpiHostHistogram<KpiHostGeneralHistogramCount>>;
};
unique_destination_ips: {
value: number;
};
unique_destination_ips_histogram: {
buckets: [
{
key_as_string: string;
key: number;
doc_count: number;
count: {
value: number;
};
}
];
buckets: Array<KpiHostHistogram<KpiHostGeneralHistogramCount>>;
};
};
_shards: {
@ -79,31 +80,13 @@ export interface KpiHostsAuthHit extends SearchHit {
doc_count: number;
};
authentication_success_histogram: {
buckets: [
{
key_as_string: string;
key: number;
doc_count: number;
count: {
doc_count: number;
};
}
];
buckets: Array<KpiHostHistogram<KpiHostAuthHistogramCount>>;
};
authentication_failure: {
doc_count: number;
};
authentication_failure_histogram: {
buckets: [
{
key_as_string: string;
key: number;
doc_count: number;
count: {
doc_count: number;
};
}
];
buckets: Array<KpiHostHistogram<KpiHostAuthHistogramCount>>;
};
};
_shards: {

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { KpiNetworkData } from '../../graphql/types';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { ElasticsearchKpiNetworkAdapter } from './elasticsearch_adapter';
@ -13,6 +12,7 @@ import * as dnsQueryDsl from './query_dns.dsl';
import * as generalQueryDsl from './query_general.dsl';
import * as tlsHandshakesQueryDsl from './query_tls_handshakes.dsl';
import * as uniquePrvateIpQueryDsl from './query_unique_private_ips.dsl';
import { KpiNetworkData } from '../../graphql/types';
describe('Network Kpi elasticsearch_adapter', () => {
const mockCallWithRequest = jest.fn();
@ -118,9 +118,10 @@ describe('Network Kpi elasticsearch_adapter', () => {
expect(data).toEqual({
networkEvents: null,
uniqueFlowId: null,
activeAgents: null,
uniqueSourcePrivateIps: null,
uniqueSourcePrivateIpsHistogram: null,
uniqueDestinationPrivateIps: null,
uniqueDestinationPrivateIpsHistogram: null,
dnsQueries: null,
tlsHandshakes: null,
});

View file

@ -6,15 +6,34 @@
import { getOr } from 'lodash/fp';
import { KpiNetworkData } from '../../graphql/types';
import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework';
import { TermAggregation } from '../types';
import { buildDnsQuery } from './query_dns.dsl';
import { buildGeneralQuery } from './query_general.dsl';
import { buildTlsHandshakeQuery } from './query_tls_handshakes.dsl';
import { buildUniquePrvateIpQuery } from './query_unique_private_ips.dsl';
import { KpiNetworkAdapter, KpiNetworkESMSearchBody, KpiNetworkHit } from './types';
import {
KpiNetworkHit,
KpiNetworkAdapter,
KpiNetworkESMSearchBody,
KpiNetworkGeneralHit,
KpiNetworkUniquePrivateIpsHit,
} from './types';
import { TermAggregation } from '../types';
import { KpiNetworkHistogramData, KpiNetworkData } from '../../graphql/types';
const formatHistogramData = (
data: Array<{ key_as_string: string; count: { value: number } }>
): KpiNetworkHistogramData[] | null => {
return data && data.length > 0
? data.map<KpiNetworkHistogramData>(({ key_as_string, count }) => {
return {
x: key_as_string,
y: getOr(null, 'value', count),
};
})
: null;
};
export class ElasticsearchKpiNetworkAdapter implements KpiNetworkAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
@ -34,34 +53,46 @@ export class ElasticsearchKpiNetworkAdapter implements KpiNetworkAdapter {
);
const dnsQuery: KpiNetworkESMSearchBody[] = buildDnsQuery(options);
const tlsHandshakesQuery: KpiNetworkESMSearchBody[] = buildTlsHandshakeQuery(options);
const response = await this.framework.callWithRequest<KpiNetworkHit, TermAggregation>(
request,
'msearch',
{
body: [
...generalQuery,
...uniqueSourcePrivateIpsQuery,
...uniqueDestinationPrivateIpsQuery,
...dnsQuery,
...tlsHandshakesQuery,
],
}
const response = await this.framework.callWithRequest<
KpiNetworkGeneralHit | KpiNetworkHit | KpiNetworkUniquePrivateIpsHit,
TermAggregation
>(request, 'msearch', {
body: [
...generalQuery,
...uniqueSourcePrivateIpsQuery,
...uniqueDestinationPrivateIpsQuery,
...dnsQuery,
...tlsHandshakesQuery,
],
});
const uniqueSourcePrivateIpsHistogram = getOr(
null,
'responses.1.aggregations.histogram.buckets',
response
);
const uniqueDestinationPrivateIpsHistogram = getOr(
null,
'responses.2.aggregations.histogram.buckets',
response
);
return {
networkEvents: getOr(null, 'responses.0.hits.total.value', response),
uniqueFlowId: getOr(null, 'responses.0.aggregations.unique_flow_id.value', response),
activeAgents: getOr(null, 'responses.0.aggregations.active_agents.value', response),
uniqueSourcePrivateIps: getOr(
null,
'responses.1.aggregations.unique_private_ips.value',
response
),
uniqueSourcePrivateIpsHistogram: formatHistogramData(uniqueSourcePrivateIpsHistogram),
uniqueDestinationPrivateIps: getOr(
null,
'responses.2.aggregations.unique_private_ips.value',
response
),
uniqueDestinationPrivateIpsHistogram: formatHistogramData(
uniqueDestinationPrivateIpsHistogram
),
dnsQueries: getOr(null, 'responses.3.hits.total.value', response),
tlsHandshakes: getOr(null, 'responses.4.hits.total.value', response),
};

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { KpiNetworkData } from '../../graphql/types';
import { FrameworkRequest, RequestBasicOptions } from '../framework';
import { KpiNetworkAdapter } from './types';
import { KpiNetworkData } from '../../graphql/types';
export class KpiNetwork {
constructor(private readonly adapter: KpiNetworkAdapter) {}

View file

@ -32,11 +32,11 @@ export const mockRequest = {
operationName: 'GetKpiNetworkQuery',
variables: {
sourceId: 'default',
timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 },
timerange: { interval: '12h', from: 1557445721842, to: 1557532121842 },
filterQuery: '',
},
query:
'query GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n networkEvents\n uniqueFlowId\n activeAgents\n uniqueSourcePrivateIps\n uniqueDestinationPrivateIps\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n',
'fragment ChartFields on KpiNetworkHistogramData {\n key_as_string\n doc_count\n count {\n value\n __typename\n }\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n networkEvents\n networkEventsHistogram {\n ...ChartFields\n __typename\n }\n uniqueFlowId\n activeAgents\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...ChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...ChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n',
},
query: {},
};
@ -44,52 +44,209 @@ export const mockRequest = {
export const mockResponse = {
responses: [
{
took: 258,
took: 384,
timed_out: false,
_shards: { total: 26, successful: 26, skipped: 0, failed: 0 },
hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] },
aggregations: { unique_flow_id: { value: 50243 }, active_agents: { value: 15 } },
_shards: {
total: 10,
successful: 10,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 733106,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
unique_flow_id: {
value: 195415,
},
},
status: 200,
},
{
took: 323,
took: 224,
timed_out: false,
_shards: { total: 26, successful: 26, skipped: 0, failed: 0 },
hits: { total: { value: 406839, relation: 'eq' }, max_score: null, hits: [] },
aggregations: { unique_private_ips: { value: 383 } },
_shards: {
total: 10,
successful: 10,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 480755,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
histogram: {
buckets: [
{
key_as_string: '2019-05-09T23:00:00.000Z',
key: 1557442800000,
doc_count: 42109,
count: {
value: 14,
},
},
{
key_as_string: '2019-05-10T11:00:00.000Z',
key: 1557486000000,
doc_count: 437160,
count: {
value: 385,
},
},
{
key_as_string: '2019-05-10T23:00:00.000Z',
key: 1557529200000,
doc_count: 1486,
count: {
value: 7,
},
},
],
interval: '12h',
},
unique_private_ips: {
value: 387,
},
},
status: 200,
},
{
took: 323,
took: 184,
timed_out: false,
_shards: { total: 26, successful: 26, skipped: 0, failed: 0 },
hits: { total: { value: 406839, relation: 'eq' }, max_score: null, hits: [] },
aggregations: { unique_private_ips: { value: 18 } },
_shards: {
total: 10,
successful: 10,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 459283,
relation: 'eq',
},
max_score: null,
hits: [],
},
aggregations: {
histogram: {
buckets: [
{
key_as_string: '2019-05-09T23:00:00.000Z',
key: 1557442800000,
doc_count: 36253,
count: {
value: 11,
},
},
{
key_as_string: '2019-05-10T11:00:00.000Z',
key: 1557486000000,
doc_count: 421719,
count: {
value: 877,
},
},
{
key_as_string: '2019-05-10T23:00:00.000Z',
key: 1557529200000,
doc_count: 1311,
count: {
value: 7,
},
},
],
interval: '12h',
},
unique_private_ips: {
value: 878,
},
},
status: 200,
},
{
took: 9,
took: 64,
timed_out: false,
_shards: { total: 38, successful: 38, skipped: 36, failed: 0 },
hits: { total: { value: 278, relation: 'eq' }, max_score: null, hits: [] },
_shards: {
total: 10,
successful: 10,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 10942,
relation: 'eq',
},
max_score: null,
hits: [],
},
status: 200,
},
{
took: 60,
took: 57,
timed_out: false,
_shards: { total: 38, successful: 38, skipped: 36, failed: 0 },
hits: { total: { value: 10000, relation: 'gte' }, max_score: null, hits: [] },
_shards: {
total: 10,
successful: 10,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 54482,
relation: 'eq',
},
max_score: null,
hits: [],
},
status: 200,
},
],
};
export const mockResult = {
networkEvents: 950867,
uniqueFlowId: 50243,
activeAgents: 15,
uniqueSourcePrivateIps: 383,
uniqueDestinationPrivateIps: 18,
dnsQueries: 278,
tlsHandshakes: 10000,
dnsQueries: 10942,
networkEvents: 733106,
tlsHandshakes: 54482,
uniqueDestinationPrivateIps: 878,
uniqueDestinationPrivateIpsHistogram: [
{
x: '2019-05-09T23:00:00.000Z',
y: 11,
},
{
x: '2019-05-10T11:00:00.000Z',
y: 877,
},
{
x: '2019-05-10T23:00:00.000Z',
y: 7,
},
],
uniqueFlowId: 195415,
uniqueSourcePrivateIps: 387,
uniqueSourcePrivateIpsHistogram: [
{
x: '2019-05-09T23:00:00.000Z',
y: 14,
},
{
x: '2019-05-10T11:00:00.000Z',
y: 385,
},
{
x: '2019-05-10T23:00:00.000Z',
y: 7,
},
],
};

View file

@ -75,11 +75,6 @@ export const buildGeneralQuery = ({
field: 'network.community_id',
},
},
active_agents: {
cardinality: {
field: 'agent.id',
},
},
},
query: {
bool: {

View file

@ -117,6 +117,19 @@ export const buildUniquePrvateIpQuery = (
field: `${attrQuery}.ip`,
},
},
histogram: {
auto_date_histogram: {
field: '@timestamp',
buckets: '6',
},
aggs: {
count: {
cardinality: {
field: `${attrQuery}.ip`,
},
},
},
},
},
query: {
bool: {

View file

@ -3,21 +3,37 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KpiNetworkData } from '../../graphql/types';
import { FrameworkRequest, RequestBasicOptions } from '../framework';
import { MSearchHeader, SearchHit } from '../types';
import { KpiNetworkHistogramData, KpiNetworkData } from '../../graphql/types';
export interface KpiNetworkAdapter {
getKpiNetwork(request: FrameworkRequest, options: RequestBasicOptions): Promise<KpiNetworkData>;
}
export interface KpiNetworkHit extends SearchHit {
export interface KpiNetworkHit {
hits: {
total: {
value: number;
};
};
}
export interface KpiNetworkGeneralHit extends SearchHit, KpiNetworkHit {
aggregations: {
unique_flow_id: {
value: number;
};
active_agents: {
value: number | null;
};
}
export interface KpiNetworkUniquePrivateIpsHit extends SearchHit {
aggregations: {
unique_private_ips: {
value: number;
};
histogram: {
buckets: [KpiNetworkHistogramData];
};
};
}
@ -32,3 +48,8 @@ export interface KpiNetworkBody {
export type KpiNetworkESMSearchBody = KpiNetworkBody | MSearchHeader;
export type UniquePrivateAttributeQuery = 'source' | 'destination';
// export interface KpiNetworkHistogram {
// x: string | null | undefined;
// y: number | null | undefined;
// }

View file

@ -9135,9 +9135,8 @@
"xpack.siem.kpiNetwork.source.loadingDescription": "読み込み中",
"xpack.siem.kpiNetwork.source.networkEventsTitle": "ネットワークイベント",
"xpack.siem.kpiNetwork.source.tlsHandshakesTitle": "TLS ハンドシェイク",
"xpack.siem.kpiNetwork.source.uniqueDestinationPrivateIpsTitle": "固有の送信先 IP",
"xpack.siem.kpiNetwork.source.uniqueSourcePrivateIpsTitle": "固有の送信元 IP",
"xpack.siem.kpiNetwork.source.uniquiIdTitle": "固有のフロー ID",
"xpack.siem.kpiNetwork.source.uniquePrivateIpsTitle": "固有の送信元 IP",
"xpack.siem.kpiNetwork.source.uniqueFlowIdsTitle": "固有のフロー ID",
"xpack.siem.loadingMoreTable.loadingDescription": "読み込み中",
"xpack.siem.loadingMoreTable.loadMoreDescription": "他を読み込む",
"xpack.siem.loadingMoreTable.showing": "表示中",

View file

@ -9145,9 +9145,8 @@
"xpack.siem.kpiNetwork.source.loadingDescription": "正在加载",
"xpack.siem.kpiNetwork.source.networkEventsTitle": "网络事件",
"xpack.siem.kpiNetwork.source.tlsHandshakesTitle": "TLS 握手",
"xpack.siem.kpiNetwork.source.uniqueDestinationPrivateIpsTitle": "唯一目标 IP",
"xpack.siem.kpiNetwork.source.uniqueSourcePrivateIpsTitle": "唯一源 IP",
"xpack.siem.kpiNetwork.source.uniquiIdTitle": "唯一流 ID",
"xpack.siem.kpiNetwork.source.uniquePrivateIpsTitle": "唯一目标 IP",
"xpack.siem.kpiNetwork.source.uniqueFlowIdsTitle": "唯一源 IP",
"xpack.siem.loadingMoreTable.loadingDescription": "正在加载",
"xpack.siem.loadingMoreTable.loadMoreDescription": "加载更多",
"xpack.siem.loadingMoreTable.showing": "显示",

View file

@ -25,81 +25,49 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => {
hostsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
],
authSuccess: 0,
authSuccessHistogram: [],
authSuccessHistogram: null,
authFailure: 0,
authFailureHistogram: [],
authFailureHistogram: null,
uniqueSourceIps: 121,
uniqueSourceIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 52,
},
y: 52,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 31,
},
y: 31,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 88,
},
y: 88,
__typename: 'KpiHostHistogramData',
},
],
@ -107,38 +75,22 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => {
uniqueDestinationIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 61,
},
y: 61,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 45,
},
y: 45,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 114,
},
y: 114,
__typename: 'KpiHostHistogramData',
},
],
@ -177,81 +129,49 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => {
hostsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 1,
},
y: 1,
__typename: 'KpiHostHistogramData',
},
],
authSuccess: 0,
authSuccessHistogram: [],
authSuccessHistogram: null,
authFailure: 0,
authFailureHistogram: [],
authFailureHistogram: null,
uniqueSourceIps: 121,
uniqueSourceIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 52,
},
y: 52,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 31,
},
y: 31,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 88,
},
y: 88,
__typename: 'KpiHostHistogramData',
},
],
@ -259,38 +179,22 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => {
uniqueDestinationIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 61,
},
y: 61,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 0,
},
y: 0,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 45,
},
y: 45,
__typename: 'KpiHostHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: {
__typename: 'Count',
doc_count: null,
value: 114,
},
y: 114,
__typename: 'KpiHostHistogramData',
},
],

View file

@ -19,6 +19,59 @@ const kpiNetworkTests: KbnTestProvider = ({ getService }) => {
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: 'KpiNetworkData',
networkEvents: 6157,
uniqueFlowId: 712,
uniqueSourcePrivateIps: 8,
uniqueSourcePrivateIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: 8,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: 0,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: 8,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: 7,
__typename: 'KpiNetworkHistogramData',
},
],
uniqueDestinationPrivateIps: 9,
uniqueDestinationPrivateIpsHistogram: [
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T16:00:00.000Z',
y: 8,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T19:00:00.000Z',
y: 0,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T22:00:00.000Z',
y: 8,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-10T01:00:00.000Z',
y: 8,
},
],
dnsQueries: 169,
tlsHandshakes: 62,
};
it('Make sure that we get KpiNetwork data', () => {
return client
@ -36,13 +89,7 @@ const kpiNetworkTests: KbnTestProvider = ({ getService }) => {
})
.then(resp => {
const kpiNetwork = resp.data.source.KpiNetwork;
expect(kpiNetwork!.networkEvents).to.be(6157);
expect(kpiNetwork!.uniqueFlowId).to.be(712);
expect(kpiNetwork!.activeAgents).to.equal(1);
expect(kpiNetwork!.uniqueSourcePrivateIps).to.equal(8);
expect(kpiNetwork!.uniqueDestinationPrivateIps).to.equal(9);
expect(kpiNetwork!.dnsQueries).to.equal(169);
expect(kpiNetwork!.tlsHandshakes).to.equal(62);
expect(kpiNetwork).to.eql(expectedResult);
});
});
});
@ -53,7 +100,59 @@ const kpiNetworkTests: KbnTestProvider = ({ getService }) => {
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: 'KpiNetworkData',
networkEvents: 6157,
uniqueFlowId: 712,
uniqueSourcePrivateIps: 8,
uniqueSourcePrivateIpsHistogram: [
{
x: '2019-02-09T16:00:00.000Z',
y: 8,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-09T19:00:00.000Z',
y: 0,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-09T22:00:00.000Z',
y: 8,
__typename: 'KpiNetworkHistogramData',
},
{
x: '2019-02-10T01:00:00.000Z',
y: 7,
__typename: 'KpiNetworkHistogramData',
},
],
uniqueDestinationPrivateIps: 9,
uniqueDestinationPrivateIpsHistogram: [
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T16:00:00.000Z',
y: 8,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T19:00:00.000Z',
y: 0,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-09T22:00:00.000Z',
y: 8,
},
{
__typename: 'KpiNetworkHistogramData',
x: '2019-02-10T01:00:00.000Z',
y: 8,
},
],
dnsQueries: 169,
tlsHandshakes: 62,
};
it('Make sure that we get KpiNetwork data', () => {
return client
.query<GetKpiNetworkQuery.Query>({
@ -70,13 +169,7 @@ const kpiNetworkTests: KbnTestProvider = ({ getService }) => {
})
.then(resp => {
const kpiNetwork = resp.data.source.KpiNetwork;
expect(kpiNetwork!.networkEvents).to.be(6157);
expect(kpiNetwork!.uniqueFlowId).to.be(712);
expect(kpiNetwork!.activeAgents).to.equal(1);
expect(kpiNetwork!.uniqueSourcePrivateIps).to.equal(8);
expect(kpiNetwork!.uniqueDestinationPrivateIps).to.equal(9);
expect(kpiNetwork!.dnsQueries).to.equal(169);
expect(kpiNetwork!.tlsHandshakes).to.equal(62);
expect(kpiNetwork).to.eql(expectedResult);
});
});
});