From 39ae445ac76f99fef965b7f5a9875adf1908c459 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sat, 6 Apr 2019 17:21:01 +0800 Subject: [PATCH 01/26] init graphql --- .../public/components/page/hosts/index.tsx | 1 + .../components/page/hosts/kpi_hosts/index.tsx | 139 +++++++++++ .../page/hosts/kpi_hosts/translations.ts | 58 +++++ .../containers/kpi_hosts/index.gql_query.ts | 27 +++ .../public/containers/kpi_hosts/index.tsx | 57 +++++ .../siem/public/graphql/introspection.json | 122 ++++++++++ x-pack/plugins/siem/public/graphql/types.ts | 77 ++++++ .../plugins/siem/public/pages/hosts/hosts.tsx | 29 ++- x-pack/plugins/siem/server/graphql/index.ts | 3 +- .../siem/server/graphql/kpi_hosts/index.ts | 8 + .../graphql/kpi_hosts/kpi_hosts.mock.ts | 40 ++++ .../graphql/kpi_hosts/resolvers.test.ts | 80 +++++++ .../server/graphql/kpi_hosts/resolvers.ts | 35 +++ .../server/graphql/kpi_hosts/schema.gql.ts | 26 ++ x-pack/plugins/siem/server/graphql/types.ts | 121 ++++++++++ x-pack/plugins/siem/server/init_server.ts | 2 + .../plugins/siem/server/lib/compose/kibana.ts | 4 + .../kpi_hosts/elasticsearch_adapter.test.ts | 143 +++++++++++ .../lib/kpi_hosts/elasticsearch_adapter.ts | 93 ++++++++ .../siem/server/lib/kpi_hosts/index.ts | 21 ++ .../plugins/siem/server/lib/kpi_hosts/mock.ts | 223 ++++++++++++++++++ .../lib/kpi_hosts/query_authentication.dsl.ts | 89 +++++++ .../server/lib/kpi_hosts/query_event.dsl.ts | 101 ++++++++ .../server/lib/kpi_hosts/query_general.dsl.ts | 79 +++++++ .../lib/kpi_hosts/query_process_count.dsl.ts | 78 ++++++ .../siem/server/lib/kpi_hosts/types.ts | 46 ++++ x-pack/plugins/siem/server/lib/types.ts | 2 + .../siem/server/utils/build_query/fields.ts | 1 + .../test/api_integration/apis/siem/index.js | 1 + .../api_integration/apis/siem/kpi_hosts.ts | 92 ++++++++ 30 files changed, 1795 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx create mode 100644 x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts create mode 100644 x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts create mode 100644 x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/index.ts create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/index.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_general.dsl.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts create mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/types.ts create mode 100644 x-pack/test/api_integration/apis/siem/kpi_hosts.ts 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..b4aee15b7adea --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx @@ -0,0 +1,139 @@ +/* + * 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 } from 'lodash/fp'; +import React from 'react'; +import { pure } from 'recompose'; + +import { KpiHostsData } from '../../../../graphql/types'; +import { CardItem, CardItems, CardItemsComponent } from '../../../card_items'; + +import * as i18n from './translations'; + +interface KpiHostsProps { + data: KpiHostsData; + loading: boolean; +} + +const rowsMapping: CardItems[][] = [ + [ + { + fields: [ + { + key: 'hosts', + description: i18n.HOSTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'installedPackages', + description: i18n.INSTALLED_PACKAGES, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'processCount', + description: i18n.PROCESS_COUNT, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'authenticationAttempts', + description: i18n.AUTHENTICATION_ATTEMPTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'auditbeatEvents', + description: i18n.AUDITBEAT_EVENTS, + value: null, + }, + ], + }, + ], + [ + { + fields: [ + { + key: 'winlogbeatEvents', + description: i18n.WINLOGBEAT_EVENTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'filebeatEvents', + description: i18n.FILEBEAT_EVENTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'sockets', + description: i18n.SOCKETS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'uniqueSourceIps', + description: i18n.UNIQUE_SOURCE_IPS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'uniqueDestinationIps', + description: i18n.UNIQUE_DESTINATION_IPS, + value: null, + }, + ], + }, + ], +]; + +export const KpiHostsComponent = pure(({ data, loading }) => { + return ( + <> + {rowsMapping.map((fieldTitleMapping, rowId) => ( + + {fieldTitleMapping.map(card => ( + + ))} + + ))} + + ); +}); + +const addValueToFields = (fields: CardItem[], data: KpiHostsData): CardItem[] => + fields.map(field => ({ ...field, value: get(field.key, data) })); 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..ab0097deb0c57 --- /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 INSTALLED_PACKAGES = i18n.translate( + 'xpack.siem.kpiHosts.source.installedPackagesTitle', + { + defaultMessage: 'Packages', + } +); + +export const PROCESS_COUNT = i18n.translate('xpack.siem.kpiHosts.source.processCountsTitle', { + defaultMessage: 'Processes', +}); + +export const AUTHENTICATION_ATTEMPTS = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationAttemptsTitle', + { + defaultMessage: 'Authentication Attempts', + } +); + +export const AUDITBEAT_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.auditbeatEventsTitle', { + defaultMessage: 'Auditbeat Events', +}); + +export const WINLOGBEAT_EVENTS = i18n.translate( + 'xpack.siem.kpiHosts.source.winlogbeatEventsTitle', + { + defaultMessage: 'Winlogbeat Events', + } +); + +export const FILEBEAT_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.filebeatEventsTitle', { + defaultMessage: 'Filebeat Events', +}); + +export const SOCKETS = i18n.translate('xpack.siem.kpiHosts.source.socketsTitle', { + defaultMessage: 'Sockets', +}); + +export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueSourceIpsTitle', { + defaultMessage: 'Unique Source Ips', +}); + +export const UNIQUE_DESTINATION_IPS = i18n.translate( + 'xpack.siem.kpiHosts.source.uniqueDestinationIpsTitle', + { + defaultMessage: 'Unique Destination Ips', + } +); 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..f35d7c82bb25b --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts @@ -0,0 +1,27 @@ +/* + * 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` + query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) { + source(id: $sourceId) { + id + KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { + hosts + installedPackages + processCount + authenticationAttempts + auditbeatEvents + winlogbeatEvents + filebeatEvents + sockets + uniqueSourceIps + uniqueDestinationIps + } + } + } +`; 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..4bd639ae1c4d1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { 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'; + +export interface KpiHostsArgs { + id: string; + kpiHosts: KpiHostsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiHostsProps extends QueryTemplateProps { + children: (args: KpiHostsArgs) => React.ReactNode; +} + +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); + return children({ + id, + kpiHosts, + loading, + refetch, + }); + }} + + ) +); diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index 8cd8d3363ca47..a58918dfb3494 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -797,6 +797,37 @@ "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": "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", @@ -6254,6 +6285,97 @@ "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": "installedPackages", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processCount", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationAttempts", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "auditbeatEvents", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "winlogbeatEvents", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filebeatEvents", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sockets", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueSourceIps", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueDestinationIps", + "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 55bb68e9ba5b5..ca138ef452990 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 | null; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1042,6 +1044,28 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + installedPackages?: number | null; + + processCount?: number | null; + + authenticationAttempts?: number | null; + + auditbeatEvents?: number | null; + + winlogbeatEvents?: number | null; + + filebeatEvents?: number | null; + + sockets?: number | null; + + uniqueSourceIps?: number | null; + + uniqueDestinationIps?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1387,6 +1411,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { id?: string | null; @@ -2357,6 +2388,52 @@ 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 | null; + }; + + export type KpiHosts = { + __typename?: 'KpiHostsData'; + + hosts?: number | null; + + installedPackages?: number | null; + + processCount?: number | null; + + authenticationAttempts?: number | null; + + auditbeatEvents?: number | null; + + winlogbeatEvents?: number | null; + + filebeatEvents?: number | null; + + sockets?: number | null; + + uniqueSourceIps?: number | null; + + uniqueDestinationIps?: number | null; + }; +} + export namespace GetKpiNetworkQuery { export type Variables = { sourceId: string; diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx index 040cb03451674..dff4f99968fc8 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; } @@ -49,6 +55,25 @@ const HostsComponent = pure(({ filterQuery }) => ( {({ to, from, setQuery }) => ( <> + + {({ kpiHosts, loading, id, refetch }) => ( + + )} + + + + ({ + source: (root: unknown, args: unknown, context: SiemContext) => { + logger.info('Mock source'); + const operationName = context.req.payload.operationName.toLowerCase(); + switch (operationName) { + case 'test': { + logger.info(`Using mock for test ${mockKpiHostsData}`); + return mockKpiHostsData; + } + default: { + return {}; + } + } + }, +}); diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts new file mode 100644 index 0000000000000..2858e43a0e583 --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { GraphQLResolveInfo } from 'graphql'; + +import { Source } from '../../graphql/types'; +import { FrameworkRequest, internalFrameworkRequest } from '../../lib/framework'; +import { KpiHosts } from '../../lib/kpi_hosts'; +import { KpiHostsAdapter } from '../../lib/kpi_hosts/types'; +import { SourceStatus } from '../../lib/source_status'; +import { Sources } from '../../lib/sources'; +import { createSourcesResolvers } from '../sources'; +import { SourcesResolversDeps } from '../sources/resolvers'; +import { mockSourcesAdapter, mockSourceStatusAdapter } from '../sources/resolvers.test'; + +import { mockKpiHostsData } from './kpi_hosts.mock'; +import { createKpiHostsResolvers, KpiHostsResolversDeps } from './resolvers'; + +const mockGetKpiHosts = jest.fn(); +mockGetKpiHosts.mockResolvedValue({ + KpiHosts: { + ...mockKpiHostsData.KpiHosts, + }, +}); +const mockKpiHostsAdapter: KpiHostsAdapter = { + getKpiHosts: mockGetKpiHosts, +}; + +const mockKpiHostsLibs: KpiHostsResolversDeps = { + kpiHosts: new KpiHosts(mockKpiHostsAdapter), +}; + +const mockSrcLibs: SourcesResolversDeps = { + sources: new Sources(mockSourcesAdapter), + sourceStatus: new SourceStatus(mockSourceStatusAdapter, new Sources(mockSourcesAdapter)), +}; + +const req: FrameworkRequest = { + [internalFrameworkRequest]: { + params: {}, + query: {}, + payload: { + operationName: 'test', + }, + }, + params: {}, + query: {}, + payload: { + operationName: 'test', + }, +}; + +const context = { req }; + +describe('Test Source Resolvers', () => { + test('Make sure that getKpiHosts have been called', async () => { + const source = await createSourcesResolvers(mockSrcLibs).Query.source( + {}, + { id: 'default' }, + context, + {} as GraphQLResolveInfo + ); + const data = await createKpiHostsResolvers(mockKpiHostsLibs).Source.KpiHosts( + source as Source, + { + timerange: { + interval: '12h', + to: 1514782800000, + from: 1546318799999, + }, + }, + context, + {} as GraphQLResolveInfo + ); + expect(mockKpiHostsAdapter.getKpiHosts).toHaveBeenCalled(); + expect(data).toEqual(mockKpiHostsData); + }); +}); diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts new file mode 100644 index 0000000000000..20b62a22b6fc6 --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.ts @@ -0,0 +1,35 @@ +/* + * 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 { SourceResolvers } from '../../graphql/types'; +import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; +import { KpiHosts } from '../../lib/kpi_hosts'; +import { createOptions } from '../../utils/build_query/create_options'; +import { QuerySourceResolver } from '../sources/resolvers'; + +export type QueryKpiHostsResolver = ChildResolverOf< + AppResolverOf, + 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..76ffda80b362f --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const kpiHostsSchema = gql` + type KpiHostsData { + hosts: Float + installedPackages: Float + processCount: Float + authenticationAttempts: Float + auditbeatEvents: Float + winlogbeatEvents: Float + filebeatEvents: Float + sockets: Float + uniqueSourceIps: Float + uniqueDestinationIps: Float + } + + 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 4672587ca36f5..b7e10977bc45c 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 | null; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1071,6 +1073,28 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + installedPackages?: number | null; + + processCount?: number | null; + + authenticationAttempts?: number | null; + + auditbeatEvents?: number | null; + + winlogbeatEvents?: number | null; + + filebeatEvents?: number | null; + + sockets?: number | null; + + uniqueSourceIps?: number | null; + + uniqueDestinationIps?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1416,6 +1440,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { id?: string | null; @@ -1622,6 +1653,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; @@ -1874,6 +1907,19 @@ export namespace SourceResolvers { filterQuery?: string | null; } + export type KpiHostsResolver< + R = KpiHostsData | null, + Parent = Source, + Context = SiemContext + > = Resolver; + export interface KpiHostsArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; + } + export type NetworkTopNFlowResolver< R = NetworkTopNFlowData, Parent = Source, @@ -5139,6 +5185,81 @@ export namespace KpiNetworkDataResolvers { > = Resolver; } +export namespace KpiHostsDataResolvers { + export interface Resolvers { + hosts?: HostsResolver; + + installedPackages?: InstalledPackagesResolver; + + processCount?: ProcessCountResolver; + + authenticationAttempts?: AuthenticationAttemptsResolver; + + auditbeatEvents?: AuditbeatEventsResolver; + + winlogbeatEvents?: WinlogbeatEventsResolver; + + filebeatEvents?: FilebeatEventsResolver; + + sockets?: SocketsResolver; + + uniqueSourceIps?: UniqueSourceIpsResolver; + + uniqueDestinationIps?: UniqueDestinationIpsResolver; + } + + export type HostsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type InstalledPackagesResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type ProcessCountResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthenticationAttemptsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuditbeatEventsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type WinlogbeatEventsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type FilebeatEventsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type SocketsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueSourceIpsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type UniqueDestinationIpsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; +} + 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..8f1411f87e2dd --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -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 { KpiHostsData } from '../../graphql/types'; +import { FrameworkAdapter, FrameworkRequest } from '../framework'; + +import { ElasticsearchKpiHostsAdapter } from './elasticsearch_adapter'; +import { mockMsearchOptions, mockOptions, mockRequest, mockResponse, mockResult } from './mock'; +import * as authQueryDsl from './query_authentication.dsl'; +import * as eventQueryDsl from './query_event.dsl'; +import * as generalQueryDsl from './query_general.dsl'; +import * as processCountDsl from './query_process_count.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 mockBuildEventQuery: jest.SpyInstance; + let mockBuildProcessQuery: 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([]); + mockBuildAuthQuery = jest.spyOn(authQueryDsl, 'buildAuthQuery').mockReturnValue([]); + + mockBuildEventQuery = jest.spyOn(eventQueryDsl, 'buildEventQuery').mockReturnValue([]); + mockBuildProcessQuery = jest.spyOn(processCountDsl, 'buildProcessQuery').mockReturnValue([]); + EsKpiHosts = new ElasticsearchKpiHostsAdapter(mockFramework); + data = await EsKpiHosts.getKpiHosts(mockRequest as FrameworkRequest, mockOptions); + }); + + afterAll(() => { + mockCallWithRequest.mockReset(); + mockBuildQuery.mockRestore(); + mockBuildProcessQuery.mockRestore(); + mockBuildAuthQuery.mockRestore(); + mockBuildEventQuery.mockRestore(); + }); + + test('should build general query with correct option', () => { + expect(mockBuildQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should build process query with correct option', () => { + expect(mockBuildProcessQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should build auth query with correct option', () => { + expect(mockBuildAuthQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should build query for auditbeat FIM event with correct option', () => { + expect(mockBuildEventQuery).toHaveBeenCalledWith( + { agentType: 'auditbeat', eventModule: 'file_integrity' }, + mockOptions + ); + }); + + test('should build query for auditbeat auditd event with correct option', () => { + expect(mockBuildEventQuery).toHaveBeenCalledWith( + { agentType: 'auditbeat', eventModule: 'auditd' }, + mockOptions + ); + }); + + test('should build query for winlogbeat event with correct option', () => { + expect(mockBuildEventQuery).toHaveBeenCalledWith({ agentType: 'winlogbeat' }, mockOptions); + }); + + test('should build query for filebeat event with correct option', () => { + expect(mockBuildEventQuery).toHaveBeenCalledWith({ agentType: 'filebeat' }, 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({ + auditbeatEvents: null, + authenticationAttempts: null, + filebeatEvents: null, + hosts: null, + installedPackages: null, + processCount: null, + sockets: null, + uniqueDestinationIps: null, + uniqueSourceIps: null, + winlogbeatEvents: 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..7b3c800b8c158 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -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 { getOr } from 'lodash/fp'; + +import { KpiHostsData } from '../../graphql/types'; +// tslint:disable-next-line: prettier +import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TermAggregation } from '../types'; + +import { buildAuthQuery } from './query_authentication.dsl'; +import { buildEventQuery } from './query_event.dsl'; +import { buildGeneralQuery } from './query_general.dsl'; +import { buildProcessQuery } from './query_process_count.dsl'; +import { KpiHostsAdapter, KpiHostsESMSearchBody, KpiHostsHit } 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 processQuery: KpiHostsESMSearchBody[] = buildProcessQuery(options); + const authQuery: KpiHostsESMSearchBody[] = buildAuthQuery(options); + const auditbeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( + { agentType: 'auditbeat' }, + options + ); + + const winlogbeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( + { agentType: 'winlogbeat' }, + options + ); + + const filebeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( + { agentType: 'filebeat' }, + options + ); + const response = await this.framework.callWithRequest( + request, + 'msearch', + { + body: [ + ...generalQuery, + ...processQuery, + ...authQuery, + ...auditbeatQuery, + ...winlogbeatQuery, + ...filebeatQuery, + ], + } + ); + return { + hosts: getOr(null, 'responses.0.aggregations.host.value', response), + installedPackages: getOr(null, 'responses.0.aggregations.installedPackages.value', response), + processCount: getOr(null, 'responses.1.hits.total.value', response), + authenticationAttempts: getAuthenticationAttempts( + getOr(null, 'responses.2.aggregations.authentication_success.doc_count', response), + getOr(null, 'responses.2.aggregations.authentication_failure.doc_count', response) + ), + auditbeatEvents: getOr(null, 'responses.3.hits.total.value', response), + winlogbeatEvents: getOr(null, 'responses.5.hits.total.value', response), + filebeatEvents: getOr(null, 'responses.6.hits.total.value', response), + sockets: getOr(null, 'responses.0.aggregations.sockets.value', response), + uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response), + uniqueDestinationIps: getOr( + null, + 'responses.0.aggregations.unique_destination_ips.value', + response + ), + }; + } +} + +const getAuthenticationAttempts = ( + authenticationSuccess: number | null, + authenticationFailure: number | null +): number | null => { + if (authenticationSuccess != null && authenticationFailure != null) { + return authenticationSuccess + authenticationFailure; + } else if (authenticationSuccess == null && authenticationFailure != null) { + return authenticationFailure; + } else if (authenticationSuccess != null && authenticationFailure == null) { + return authenticationSuccess; + } else { + return null; + } +}; 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..2cd0e3b67abe1 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -0,0 +1,223 @@ +/* + * 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 mockMsearchOptions = { + body: [], +}; + +export const mockRequest = { + params: {}, + payload: { + operationName: 'GetKpiHostsQuery', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + filterQuery: '', + }, + query: + 'query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery) {\n hosts\n installedPackages\n processCount\n authenticationAttempts\n auditbeatEvents\n winlogbeatEvents\n filebeatEvents\n sockets\n uniqueSourceIps\n uniqueDestinationIps\n __typename\n }\n __typename\n }\n}\n', + }, + query: {}, +}; + +export const mockResponse = { + took: 577, + responses: [ + { + took: 577, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 1225373, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + unique_source_ips: { + value: 7600, + }, + host: { + value: 6, + }, + unique_destination_ips: { + value: 1946, + }, + sockets: { + value: 0, + }, + installedPackages: { + value: 0, + }, + }, + status: 200, + }, + { + took: 265, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 11, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 243, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 27, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + authentication_success: { + doc_count: 27, + }, + authentication_failure: { + doc_count: 0, + }, + }, + status: 200, + }, + { + took: 231, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 273, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 240, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 8787, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + { + took: 231, + timed_out: false, + _shards: { + total: 47, + successful: 47, + skipped: 40, + failed: 0, + }, + hits: { + total: { + value: 956933, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + status: 200, + }, + ], +}; + +export const mockResult = { + auditbeatEvents: 0, + authenticationFailure: 0, + authenticationAttempts: 27, + filebeatEvents: 956933, + hosts: 6, + installedPackages: 0, + processCount: 11, + sockets: 0, + uniqueDestinationIps: 1946, + uniqueSourceIps: 7600, + winlogbeatEvents: 8787, +}; 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..c05c5fec2f9ae --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_authentication.dsl.ts @@ -0,0 +1,89 @@ +/* + * 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_failure: { + filter: { + term: { + 'event.type': 'authentication_failure', + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + ]; + + return dslQuery; +}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts new file mode 100644 index 0000000000000..b801c259804b2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts @@ -0,0 +1,101 @@ +/* + * 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 { EventModuleAttributeQuery, KpiHostsESMSearchBody } from './types'; + +const getAgentTypeFilter = ({ agentType }: EventModuleAttributeQuery) => + agentType + ? [ + { + bool: { + should: [ + { + match_phrase: { + 'agent.type': agentType, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ] + : []; + +const getEventModuleFilter = ({ agentType }: EventModuleAttributeQuery) => + agentType === 'auditbeat' + ? [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': 'file_integrity', + }, + }, + { + match_phrase: { + 'event.module': 'auditd', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ] + : []; + +const getEventQueryFilter = (attrQuery: EventModuleAttributeQuery) => [ + ...getAgentTypeFilter(attrQuery), + ...getEventModuleFilter(attrQuery), +]; +export const buildEventQuery = ( + attrQuery: EventModuleAttributeQuery, + { + filterQuery, + timerange: { from, to }, + sourceConfiguration: { + fields: { timestamp }, + logAlias, + auditbeatAlias, + packetbeatAlias, + winlogbeatAlias, + }, + }: RequestBasicOptions +): KpiHostsESMSearchBody[] => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + ...getEventQueryFilter(attrQuery), + { + range: { + [timestamp]: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = [ + { + index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + ]; + + 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..ad69487c9a19a --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_general.dsl.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createQueryFilterClauses } 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: { + host: { + cardinality: { + field: 'host.name', + }, + }, + installedPackages: { + cardinality: { + field: 'system.audit.package.entity_id', + }, + }, + sockets: { + cardinality: { + field: 'socket.entity_id', + }, + }, + unique_source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + unique_destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + ]; + + return dslQuery; +}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts new file mode 100644 index 0000000000000..9905042094c28 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.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 { createQueryFilterClauses } from '../../utils/build_query'; +import { RequestBasicOptions } from '../framework'; + +import { KpiHostsESMSearchBody } from './types'; + +const getProcessQueryFilter = () => [ + { + bool: { + should: [ + { + match: { + 'event.action': 'process_started', + }, + }, + { + match: { + 'event.action': 'executed', + }, + }, + { + match: { + 'event.code': 4688, + }, + }, + ], + minimum_should_match: 1, + }, + }, +]; + +export const buildProcessQuery = ({ + filterQuery, + timerange: { from, to }, + sourceConfiguration: { + fields: { timestamp }, + logAlias, + auditbeatAlias, + packetbeatAlias, + winlogbeatAlias, + }, +}: RequestBasicOptions): KpiHostsESMSearchBody[] => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + ...getProcessQueryFilter(), + { + range: { + [timestamp]: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = [ + { + index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + ]; + + 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..a1894d9edef5d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts @@ -0,0 +1,46 @@ +/* + * 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 KpiHostsHit extends SearchHit { + aggregations: { + host: { + value: number; + }; + installedPackages: { + value: number; + }; + processCount: { + value: number; + }; + authenticationSuccess: { + value: number; + }; + authenticationFailure: { + value: 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..60d167609edc1 --- /dev/null +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -0,0 +1,92 @@ +/* + * 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(); + + 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!.hosts).to.be(1); + expect(kpiHosts!.installedPackages).to.be(0); + expect(kpiHosts!.processCount).to.equal(0); + expect(kpiHosts!.authenticationAttempts).to.equal(0); + expect(kpiHosts!.auditbeatEvents).to.equal(0); + expect(kpiHosts!.winlogbeatEvents).to.equal(0); + expect(kpiHosts!.filebeatEvents).to.equal(6157); + expect(kpiHosts!.sockets).to.equal(0); + expect(kpiHosts!.uniqueSourceIps).to.equal(121); + expect(kpiHosts!.uniqueDestinationIps).to.equal(154); + }); + }); + }); + + 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(); + + 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!.hosts).to.be(1); + expect(kpiHosts!.installedPackages).to.be(0); + expect(kpiHosts!.processCount).to.equal(0); + expect(kpiHosts!.authenticationAttempts).to.equal(0); + expect(kpiHosts!.auditbeatEvents).to.equal(0); + expect(kpiHosts!.winlogbeatEvents).to.equal(0); + expect(kpiHosts!.filebeatEvents).to.equal(6157); + expect(kpiHosts!.sockets).to.equal(0); + expect(kpiHosts!.uniqueSourceIps).to.equal(121); + expect(kpiHosts!.uniqueDestinationIps).to.equal(154); + }); + }); + }); + }); +}; + +// tslint:disable-next-line no-default-export +export default kpiHostsTests; From 5a9863e01f12ce8f72ecb18007b2ff8117e972f8 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sat, 20 Apr 2019 09:34:02 +0800 Subject: [PATCH 02/26] update KPIs --- .../public/components/card_items/index.tsx | 79 ------- .../components/page/hosts/kpi_hosts/index.tsx | 176 ++++++-------- .../page/hosts/kpi_hosts/translations.ts | 35 +-- .../page/network/kpi_network/index.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../{card_items => stat_items}/index.test.tsx | 20 +- .../public/components/stat_items/index.tsx | 82 +++++++ .../containers/kpi_hosts/index.gql_query.ts | 12 +- .../siem/public/graphql/introspection.json | 61 +++-- x-pack/plugins/siem/public/graphql/types.ts | 44 ++-- .../graphql/kpi_hosts/kpi_hosts.mock.ts | 12 +- .../server/graphql/kpi_hosts/schema.gql.ts | 16 +- .../server/graphql/kpi_hosts/schema.test.ts | 113 +++++++++ x-pack/plugins/siem/server/graphql/types.ts | 91 +++----- .../kpi_hosts/elasticsearch_adapter.test.ts | 66 ++---- .../lib/kpi_hosts/elasticsearch_adapter.ts | 60 +---- .../plugins/siem/server/lib/kpi_hosts/mock.ts | 214 +++++++----------- .../lib/kpi_hosts/query_authentication.dsl.ts | 18 +- .../server/lib/kpi_hosts/query_event.dsl.ts | 101 --------- .../server/lib/kpi_hosts/query_general.dsl.ts | 13 +- .../lib/kpi_hosts/query_process_count.dsl.ts | 78 ------- .../siem/server/lib/kpi_hosts/types.ts | 11 +- .../api_integration/apis/siem/kpi_hosts.ts | 23 +- 23 files changed, 535 insertions(+), 802 deletions(-) delete mode 100644 x-pack/plugins/siem/public/components/card_items/index.tsx rename x-pack/plugins/siem/public/components/{card_items => stat_items}/__snapshots__/index.test.tsx.snap (77%) rename x-pack/plugins/siem/public/components/{card_items => stat_items}/index.test.tsx (76%) create mode 100644 x-pack/plugins/siem/public/components/stat_items/index.tsx create mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts delete mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts delete mode 100644 x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts 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/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx index b4aee15b7adea..4c1c4f1b694ad 100644 --- 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 @@ -10,7 +10,7 @@ import React from 'react'; import { pure } from 'recompose'; import { KpiHostsData } from '../../../../graphql/types'; -import { CardItem, CardItems, CardItemsComponent } from '../../../card_items'; +import { StatItem, StatItems, StatItemsComponent } from '../../../stat_items'; import * as i18n from './translations'; @@ -19,121 +19,77 @@ interface KpiHostsProps { loading: boolean; } -const rowsMapping: CardItems[][] = [ - [ - { - fields: [ - { - key: 'hosts', - description: i18n.HOSTS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'installedPackages', - description: i18n.INSTALLED_PACKAGES, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'processCount', - description: i18n.PROCESS_COUNT, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'authenticationAttempts', - description: i18n.AUTHENTICATION_ATTEMPTS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'auditbeatEvents', - description: i18n.AUDITBEAT_EVENTS, - value: null, - }, - ], - }, - ], - [ - { - fields: [ - { - key: 'winlogbeatEvents', - description: i18n.WINLOGBEAT_EVENTS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'filebeatEvents', - description: i18n.FILEBEAT_EVENTS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'sockets', - description: i18n.SOCKETS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'uniqueSourceIps', - description: i18n.UNIQUE_SOURCE_IPS, - value: null, - }, - ], - }, - { - fields: [ - { - key: 'uniqueDestinationIps', - description: i18n.UNIQUE_DESTINATION_IPS, - value: null, - }, - ], - }, - ], +const fieldTitleMapping: StatItems[] = [ + { + fields: [ + { + key: 'hosts', + description: i18n.HOSTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'agents', + description: i18n.AGENTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'authentication.success', + description: i18n.AUTHENTICATION_SUCCESS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'authentication.failure', + description: i18n.AUTHENTICATION_FAILURE, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'uniqueSourceIps', + description: i18n.UNIQUE_SOURCE_IPS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'uniqueDestinationIps', + description: i18n.UNIQUE_DESTINATION_IPS, + value: null, + }, + ], + }, ]; export const KpiHostsComponent = pure(({ data, loading }) => { return ( - <> - {rowsMapping.map((fieldTitleMapping, rowId) => ( - - {fieldTitleMapping.map(card => ( - - ))} - + + {fieldTitleMapping.map(card => ( + ))} - + ); }); -const addValueToFields = (fields: CardItem[], data: KpiHostsData): CardItem[] => +const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); 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 index ab0097deb0c57..165023f2ad4d0 100644 --- 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 @@ -9,41 +9,26 @@ export const HOSTS = i18n.translate('xpack.siem.kpiHosts.source.hostsTitle', { defaultMessage: 'Hosts', }); -export const INSTALLED_PACKAGES = i18n.translate( - 'xpack.siem.kpiHosts.source.installedPackagesTitle', - { - defaultMessage: 'Packages', - } -); - -export const PROCESS_COUNT = i18n.translate('xpack.siem.kpiHosts.source.processCountsTitle', { - defaultMessage: 'Processes', +export const AGENTS = i18n.translate('xpack.siem.kpiHosts.source.agentsTitle', { + defaultMessage: 'Agents', }); -export const AUTHENTICATION_ATTEMPTS = i18n.translate( - 'xpack.siem.kpiHosts.source.authenticationAttemptsTitle', +export const AUTHENTICATION_SUCCESS = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationSuccessTitle', { - defaultMessage: 'Authentication Attempts', + defaultMessage: 'Authentication Success', } ); -export const AUDITBEAT_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.auditbeatEventsTitle', { - defaultMessage: 'Auditbeat Events', -}); - -export const WINLOGBEAT_EVENTS = i18n.translate( - 'xpack.siem.kpiHosts.source.winlogbeatEventsTitle', +export const AUTHENTICATION_FAILURE = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationFailureTitle', { - defaultMessage: 'Winlogbeat Events', + defaultMessage: 'Authentication Failure', } ); -export const FILEBEAT_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.filebeatEventsTitle', { - defaultMessage: 'Filebeat Events', -}); - -export const SOCKETS = i18n.translate('xpack.siem.kpiHosts.source.socketsTitle', { - defaultMessage: 'Sockets', +export const ACTIVE_USERS = i18n.translate('xpack.siem.kpiHosts.source.activeUsersTitle', { + defaultMessage: 'Active Users', }); export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueSourceIpsTitle', { 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..96a8040d8606c 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,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; -import { CardItem, CardItems, CardItemsComponent } from '../../../../components/card_items'; +import { StatItem, StatItems, StatItemsComponent } from '../../../../components/stat_items'; import { KpiNetworkData } from '../../../../graphql/types'; import * as i18n from './translations'; @@ -19,7 +19,7 @@ interface KpiNetworkProps { loading: boolean; } -const fieldTitleMapping: Readonly = [ +const fieldTitleMapping: Readonly = [ { fields: [ { @@ -89,7 +89,7 @@ export const KpiNetworkComponent = pure(({ data, loading }) => return ( {fieldTitleMapping.map(card => ( - (({ data, loading }) => ); }); -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/card_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap similarity index 77% rename from x-pack/plugins/siem/public/components/card_items/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 4fb9a0719381f..f8a07e6aa01f6 100644 --- a/x-pack/plugins/siem/public/components/card_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Card Items rendering it renders loading icons 1`] = ` +exports[`Stat Items rendering it renders loading icons 1`] = ` `; -exports[`Card Items rendering it renders the default widget 1`] = ` +exports[`Stat Items rendering it renders the default widget 1`] = ` { +describe('Stat Items', () => { describe('rendering', () => { test('it renders loading icons', () => { - const mockCardItemsData: CardItemsProps = { + const mockStatItemsData: StatItemsProps = { fields: [ { key: 'networkEvents', @@ -28,12 +28,12 @@ describe('Card Items', () => { isLoading: true, key: 'mock-key', }; - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it renders the default widget', () => { - const mockCardItemsData: CardItemsProps = { + const mockStatItemsData: StatItemsProps = { fields: [ { key: 'networkEvents', @@ -44,12 +44,12 @@ describe('Card Items', () => { isLoading: false, key: 'mock-key', }; - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); it('should handle multiple titles', () => { - const mockCardItemsData: CardItemsProps = { + const mockStatItemsData: StatItemsProps = { fields: [ { key: 'uniqueSourcePrivateIps', @@ -66,8 +66,8 @@ describe('Card Items', () => { isLoading: false, key: 'mock-keys', }; - const wrapper = mount(); - expect(wrapper.find(EuiCard).prop('title')).toHaveLength(2); + const wrapper = mount(); + expect(wrapper.find(EuiStat).prop('title')).toHaveLength(2); }); }); }); 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..141e46fa4af9a --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -0,0 +1,82 @@ +/* + * 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, + EuiLoadingSpinner, + EuiPanel, + // @ts-ignore + EuiStat, +} from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React from 'react'; +import { pure } from 'recompose'; + +import { getEmptyTagValue } from '../empty_value'; + +export interface StatItem { + key: string; + description: string; + value: number | undefined | null; +} + +export interface StatItems { + fields: StatItem[]; + description?: string; +} + +export interface StatItemsProps extends StatItems { + 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 StatItemsComponent = pure( + ({ fields, description, isLoading, key }) => ( + + + {fields.length === 1 ? ( + } + description={fields[0].description} + /> + ) : ( + ( + + + + + + {field.description} + + + ))} + description={description} + /> + )} + + + ) +); 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 index f35d7c82bb25b..3eafa21ff3f15 100644 --- 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 @@ -12,13 +12,11 @@ export const kpiHostsQuery = gql` id KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { hosts - installedPackages - processCount - authenticationAttempts - auditbeatEvents - winlogbeatEvents - filebeatEvents - sockets + agents + authentication { + success + failure + } uniqueSourceIps uniqueDestinationIps } diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index a58918dfb3494..fdb08d3d44ac0 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -824,7 +824,11 @@ "defaultValue": null } ], - "type": { "kind": "OBJECT", "name": "KpiHostsData", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "KpiHostsData", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, @@ -6299,31 +6303,7 @@ "deprecationReason": null }, { - "name": "installedPackages", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "processCount", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "authenticationAttempts", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatEvents", + "name": "agents", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -6331,15 +6311,19 @@ "deprecationReason": null }, { - "name": "winlogbeatEvents", + "name": "authentication", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "authenticationData", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "filebeatEvents", + "name": "uniqueSourceIps", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -6347,15 +6331,26 @@ "deprecationReason": null }, { - "name": "sockets", + "name": "uniqueDestinationIps", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "authenticationData", + "description": "", + "fields": [ { - "name": "uniqueSourceIps", + "name": "success", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -6363,7 +6358,7 @@ "deprecationReason": null }, { - "name": "uniqueDestinationIps", + "name": "failure", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index ca138ef452990..7d59f19ac81d3 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -73,7 +73,7 @@ export interface Source { KpiNetwork?: KpiNetworkData | null; - KpiHosts?: KpiHostsData | null; + KpiHosts: KpiHostsData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1047,25 +1047,21 @@ export interface KpiNetworkData { export interface KpiHostsData { hosts?: number | null; - installedPackages?: number | null; + agents?: number | null; - processCount?: number | null; - - authenticationAttempts?: number | null; - - auditbeatEvents?: number | null; - - winlogbeatEvents?: number | null; - - filebeatEvents?: number | null; - - sockets?: number | null; + authentication: AuthenticationData; uniqueSourceIps?: number | null; uniqueDestinationIps?: number | null; } +export interface AuthenticationData { + success?: number | null; + + failure?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -2406,7 +2402,7 @@ export namespace GetKpiHostsQuery { id: string; - KpiHosts?: KpiHosts | null; + KpiHosts: KpiHosts; }; export type KpiHosts = { @@ -2414,23 +2410,21 @@ export namespace GetKpiHostsQuery { hosts?: number | null; - installedPackages?: number | null; + agents?: number | null; - processCount?: number | null; + authentication: Authentication; - authenticationAttempts?: number | null; - - auditbeatEvents?: number | null; - - winlogbeatEvents?: number | null; + uniqueSourceIps?: number | null; - filebeatEvents?: number | null; + uniqueDestinationIps?: number | null; + }; - sockets?: number | null; + export type Authentication = { + __typename?: 'authenticationData'; - uniqueSourceIps?: number | null; + success?: number | null; - uniqueDestinationIps?: number | null; + failure?: number | null; }; } diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts index 20b6612290f87..f25781ffb207f 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts @@ -11,13 +11,11 @@ import { KpiHostsData } from '../types'; export const mockKpiHostsData: { KpiHosts: KpiHostsData } = { KpiHosts: { hosts: 0, - installedPackages: 0, - processCount: 0, - authenticationAttempts: 0, - auditbeatEvents: 0, - winlogbeatEvents: 0, - filebeatEvents: 0, - sockets: 0, + agents: 0, + authentication: { + success: 0, + failure: 0, + }, uniqueSourceIps: 0, uniqueDestinationIps: 0, }, 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 index 76ffda80b362f..768182c3dd9df 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts @@ -7,20 +7,20 @@ import gql from 'graphql-tag'; export const kpiHostsSchema = gql` + type authenticationData { + success: Float + failure: Float + } + type KpiHostsData { hosts: Float - installedPackages: Float - processCount: Float - authenticationAttempts: Float - auditbeatEvents: Float - winlogbeatEvents: Float - filebeatEvents: Float - sockets: Float + agents: Float + authentication: authenticationData! uniqueSourceIps: Float uniqueDestinationIps: Float } extend type Source { - KpiHosts(id: String, timerange: TimerangeInput!, filterQuery: String): KpiHostsData + KpiHosts(id: String, timerange: TimerangeInput!, filterQuery: String): KpiHostsData! } `; diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts new file mode 100644 index 0000000000000..5412d24b33794 --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { graphql } from 'graphql'; +import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; + +import { rootSchema } from '../../../common/graphql/root/schema.gql'; +import { sharedSchema } from '../../../common/graphql/shared'; +import { Logger } from '../../utils/logger'; +import { ecsSchema } from '../ecs'; +import { dateSchema } from '../scalar_date'; +import { toBooleanSchema } from '../scalar_to_boolean_array'; +import { toDateSchema } from '../scalar_to_date_array'; +import { toNumberSchema } from '../scalar_to_number_array'; +import { sourceStatusSchema } from '../source_status/schema.gql'; +import { sourcesSchema } from '../sources/schema.gql'; + +import { getKpiHostsQueryMock, mockKpiHostsData } from './kpi_hosts.mock'; +import { kpiHostsSchema } from './schema.gql'; + +const testKpiHostsSource = { + id: 'Test case to query Siem Hosts KPIs data', + query: ` + query GetOverviewHostsQuery( + $timerange: TimerangeInput! + $filterQuery: String + ) { + source(id: "default") { + KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { + hosts + agents + authentication { + success + failure + } + uniqueSourceIps + uniqueDestinationIps + } + } + } + `, + variables: { + timerange: { + interval: '12h', + to: 1514782800000, + from: 1546318799999, + }, + }, + context: { + req: { + payload: { + operationName: 'test', + }, + }, + }, + expected: { + data: { + source: { + ...mockKpiHostsData, + }, + }, + }, +}; + +describe('SIEM Hosts GQL Schema', () => { + describe('Test KPI Hosts Schema', () => { + // Array of case types + const cases = [testKpiHostsSource]; + const typeDefs = [ + rootSchema, + sharedSchema, + sourcesSchema, + sourceStatusSchema, + ecsSchema, + kpiHostsSchema, + dateSchema, + toNumberSchema, + toDateSchema, + toBooleanSchema, + ]; + const mockSchema = makeExecutableSchema({ typeDefs }); + + // Here we specify the return payloads of mocked types + const logger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + const mocks = { + Query: () => ({ + ...getKpiHostsQueryMock(logger), + }), + }; + + addMockFunctionsToSchema({ + schema: mockSchema, + mocks, + }); + + cases.forEach(obj => { + const { id, query, variables, context, expected } = obj; + + test(`${id}`, async () => { + const result = await graphql(mockSchema, query, null, context, variables); + return await expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index b7e10977bc45c..5f8b91fab827a 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -102,7 +102,7 @@ export interface Source { KpiNetwork?: KpiNetworkData | null; - KpiHosts?: KpiHostsData | null; + KpiHosts: KpiHostsData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow: NetworkTopNFlowData; @@ -1076,25 +1076,21 @@ export interface KpiNetworkData { export interface KpiHostsData { hosts?: number | null; - installedPackages?: number | null; + agents?: number | null; - processCount?: number | null; - - authenticationAttempts?: number | null; - - auditbeatEvents?: number | null; - - winlogbeatEvents?: number | null; - - filebeatEvents?: number | null; - - sockets?: number | null; + authentication: AuthenticationData; uniqueSourceIps?: number | null; uniqueDestinationIps?: number | null; } +export interface AuthenticationData { + success?: number | null; + + failure?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1654,7 +1650,7 @@ export namespace SourceResolvers { KpiNetwork?: KpiNetworkResolver; - KpiHosts?: KpiHostsResolver; + KpiHosts?: KpiHostsResolver; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ NetworkTopNFlow?: NetworkTopNFlowResolver; @@ -1907,11 +1903,12 @@ export namespace SourceResolvers { filterQuery?: string | null; } - export type KpiHostsResolver< - R = KpiHostsData | null, - Parent = Source, - Context = SiemContext - > = Resolver; + export type KpiHostsResolver = Resolver< + R, + Parent, + Context, + KpiHostsArgs + >; export interface KpiHostsArgs { id?: string | null; @@ -5189,19 +5186,9 @@ export namespace KpiHostsDataResolvers { export interface Resolvers { hosts?: HostsResolver; - installedPackages?: InstalledPackagesResolver; - - processCount?: ProcessCountResolver; - - authenticationAttempts?: AuthenticationAttemptsResolver; - - auditbeatEvents?: AuditbeatEventsResolver; + agents?: AgentsResolver; - winlogbeatEvents?: WinlogbeatEventsResolver; - - filebeatEvents?: FilebeatEventsResolver; - - sockets?: SocketsResolver; + authentication?: AuthenticationResolver; uniqueSourceIps?: UniqueSourceIpsResolver; @@ -5213,49 +5200,43 @@ export namespace KpiHostsDataResolvers { Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type InstalledPackagesResolver< + export type AgentsResolver< R = number | null, Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type ProcessCountResolver< - R = number | null, + export type AuthenticationResolver< + R = AuthenticationData, Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type AuthenticationAttemptsResolver< - R = number | null, - Parent = KpiHostsData, - Context = SiemContext - > = Resolver; - export type AuditbeatEventsResolver< - R = number | null, - Parent = KpiHostsData, - Context = SiemContext - > = Resolver; - export type WinlogbeatEventsResolver< - R = number | null, - Parent = KpiHostsData, - Context = SiemContext - > = Resolver; - export type FilebeatEventsResolver< + export type UniqueSourceIpsResolver< R = number | null, Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type SocketsResolver< + export type UniqueDestinationIpsResolver< R = number | null, Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type UniqueSourceIpsResolver< +} + +export namespace AuthenticationDataResolvers { + export interface Resolvers { + success?: SuccessResolver; + + failure?: FailureResolver; + } + + export type SuccessResolver< R = number | null, - Parent = KpiHostsData, + Parent = AuthenticationData, Context = SiemContext > = Resolver; - export type UniqueDestinationIpsResolver< + export type FailureResolver< R = number | null, - Parent = KpiHostsData, + Parent = AuthenticationData, Context = SiemContext > = Resolver; } 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 index 8f1411f87e2dd..ae5bbd0f3f077 100644 --- 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 @@ -8,11 +8,17 @@ import { KpiHostsData } from '../../graphql/types'; import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { ElasticsearchKpiHostsAdapter } from './elasticsearch_adapter'; -import { mockMsearchOptions, mockOptions, mockRequest, mockResponse, mockResult } from './mock'; +import { + mockAuthQuery, + mockGeneralQuery, + mockMsearchOptions, + mockOptions, + mockRequest, + mockResponse, + mockResult, +} from './mock'; import * as authQueryDsl from './query_authentication.dsl'; -import * as eventQueryDsl from './query_event.dsl'; import * as generalQueryDsl from './query_general.dsl'; -import * as processCountDsl from './query_process_count.dsl'; describe('Hosts Kpi elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); @@ -25,8 +31,6 @@ describe('Hosts Kpi elasticsearch_adapter', () => { }; let mockBuildQuery: jest.SpyInstance; let mockBuildAuthQuery: jest.SpyInstance; - let mockBuildEventQuery: jest.SpyInstance; - let mockBuildProcessQuery: jest.SpyInstance; let EsKpiHosts: ElasticsearchKpiHostsAdapter; let data: KpiHostsData; @@ -36,11 +40,13 @@ describe('Hosts Kpi elasticsearch_adapter', () => { jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest, })); - mockBuildQuery = jest.spyOn(generalQueryDsl, 'buildGeneralQuery').mockReturnValue([]); - mockBuildAuthQuery = jest.spyOn(authQueryDsl, 'buildAuthQuery').mockReturnValue([]); + mockBuildQuery = jest + .spyOn(generalQueryDsl, 'buildGeneralQuery') + .mockReturnValue(mockGeneralQuery); + mockBuildAuthQuery = jest + .spyOn(authQueryDsl, 'buildAuthQuery') + .mockReturnValue(mockAuthQuery); - mockBuildEventQuery = jest.spyOn(eventQueryDsl, 'buildEventQuery').mockReturnValue([]); - mockBuildProcessQuery = jest.spyOn(processCountDsl, 'buildProcessQuery').mockReturnValue([]); EsKpiHosts = new ElasticsearchKpiHostsAdapter(mockFramework); data = await EsKpiHosts.getKpiHosts(mockRequest as FrameworkRequest, mockOptions); }); @@ -48,45 +54,17 @@ describe('Hosts Kpi elasticsearch_adapter', () => { afterAll(() => { mockCallWithRequest.mockReset(); mockBuildQuery.mockRestore(); - mockBuildProcessQuery.mockRestore(); mockBuildAuthQuery.mockRestore(); - mockBuildEventQuery.mockRestore(); }); test('should build general query with correct option', () => { expect(mockBuildQuery).toHaveBeenCalledWith(mockOptions); }); - test('should build process query with correct option', () => { - expect(mockBuildProcessQuery).toHaveBeenCalledWith(mockOptions); - }); - test('should build auth query with correct option', () => { expect(mockBuildAuthQuery).toHaveBeenCalledWith(mockOptions); }); - test('should build query for auditbeat FIM event with correct option', () => { - expect(mockBuildEventQuery).toHaveBeenCalledWith( - { agentType: 'auditbeat', eventModule: 'file_integrity' }, - mockOptions - ); - }); - - test('should build query for auditbeat auditd event with correct option', () => { - expect(mockBuildEventQuery).toHaveBeenCalledWith( - { agentType: 'auditbeat', eventModule: 'auditd' }, - mockOptions - ); - }); - - test('should build query for winlogbeat event with correct option', () => { - expect(mockBuildEventQuery).toHaveBeenCalledWith({ agentType: 'winlogbeat' }, mockOptions); - }); - - test('should build query for filebeat event with correct option', () => { - expect(mockBuildEventQuery).toHaveBeenCalledWith({ agentType: 'filebeat' }, mockOptions); - }); - test('should send msearch request', () => { expect(mockCallWithRequest).toHaveBeenCalledWith(mockRequest, 'msearch', mockMsearchOptions); }); @@ -127,16 +105,14 @@ describe('Hosts Kpi elasticsearch_adapter', () => { test('getKpiHosts - response without data', async () => { expect(data).toEqual({ - auditbeatEvents: null, - authenticationAttempts: null, - filebeatEvents: null, hosts: null, - installedPackages: null, - processCount: null, - sockets: null, - uniqueDestinationIps: null, + agents: null, + authentication: { + success: null, + failure: null, + }, uniqueSourceIps: null, - winlogbeatEvents: null, + uniqueDestinationIps: 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 index 7b3c800b8c158..e20b40e7be7d4 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -7,14 +7,11 @@ import { getOr } from 'lodash/fp'; import { KpiHostsData } from '../../graphql/types'; -// tslint:disable-next-line: prettier import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; import { TermAggregation } from '../types'; import { buildAuthQuery } from './query_authentication.dsl'; -import { buildEventQuery } from './query_event.dsl'; import { buildGeneralQuery } from './query_general.dsl'; -import { buildProcessQuery } from './query_process_count.dsl'; import { KpiHostsAdapter, KpiHostsESMSearchBody, KpiHostsHit } from './types'; export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { @@ -25,48 +22,22 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { options: RequestBasicOptions ): Promise { const generalQuery: KpiHostsESMSearchBody[] = buildGeneralQuery(options); - const processQuery: KpiHostsESMSearchBody[] = buildProcessQuery(options); const authQuery: KpiHostsESMSearchBody[] = buildAuthQuery(options); - const auditbeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( - { agentType: 'auditbeat' }, - options - ); - - const winlogbeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( - { agentType: 'winlogbeat' }, - options - ); - - const filebeatQuery: KpiHostsESMSearchBody[] = buildEventQuery( - { agentType: 'filebeat' }, - options - ); const response = await this.framework.callWithRequest( request, 'msearch', { - body: [ - ...generalQuery, - ...processQuery, - ...authQuery, - ...auditbeatQuery, - ...winlogbeatQuery, - ...filebeatQuery, - ], + body: [...generalQuery, ...authQuery], } ); + return { - hosts: getOr(null, 'responses.0.aggregations.host.value', response), - installedPackages: getOr(null, 'responses.0.aggregations.installedPackages.value', response), - processCount: getOr(null, 'responses.1.hits.total.value', response), - authenticationAttempts: getAuthenticationAttempts( - getOr(null, 'responses.2.aggregations.authentication_success.doc_count', response), - getOr(null, 'responses.2.aggregations.authentication_failure.doc_count', response) - ), - auditbeatEvents: getOr(null, 'responses.3.hits.total.value', response), - winlogbeatEvents: getOr(null, 'responses.5.hits.total.value', response), - filebeatEvents: getOr(null, 'responses.6.hits.total.value', response), - sockets: getOr(null, 'responses.0.aggregations.sockets.value', response), + hosts: getOr(null, 'responses.0.aggregations.hosts.value', response), + agents: getOr(null, 'responses.0.aggregations.agents.value', response), + authentication: { + success: getOr(null, 'responses.1.aggregations.authentication_success.doc_count', response), + failure: getOr(null, 'responses.1.aggregations.authentication_failure.doc_count', response), + }, uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response), uniqueDestinationIps: getOr( null, @@ -76,18 +47,3 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { }; } } - -const getAuthenticationAttempts = ( - authenticationSuccess: number | null, - authenticationFailure: number | null -): number | null => { - if (authenticationSuccess != null && authenticationFailure != null) { - return authenticationSuccess + authenticationFailure; - } else if (authenticationSuccess == null && authenticationFailure != null) { - return authenticationFailure; - } else if (authenticationSuccess != null && authenticationFailure == null) { - return authenticationSuccess; - } else { - return null; - } -}; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts index 2cd0e3b67abe1..5b401c383db3f 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -25,10 +25,6 @@ export const mockOptions: RequestBasicOptions = { filterQuery: {}, }; -export const mockMsearchOptions = { - body: [], -}; - export const mockRequest = { params: {}, payload: { @@ -39,7 +35,7 @@ export const mockRequest = { filterQuery: '', }, query: - 'query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery) {\n hosts\n installedPackages\n processCount\n authenticationAttempts\n auditbeatEvents\n winlogbeatEvents\n filebeatEvents\n sockets\n uniqueSourceIps\n uniqueDestinationIps\n __typename\n }\n __typename\n }\n}\n', + 'query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery) {\n hosts\n agents\n authentication {\n success\n failure\n __typename\n }\n uniqueSourceIps\n uniqueDestinationIps\n __typename\n }\n __typename\n }\n}\n', }, query: {}, }; @@ -48,17 +44,17 @@ export const mockResponse = { took: 577, responses: [ { - took: 577, + took: 2603, timed_out: false, _shards: { - total: 47, - successful: 47, - skipped: 40, + total: 67, + successful: 67, + skipped: 60, failed: 0, }, hits: { total: { - value: 1225373, + value: 9665113, relation: 'eq', }, max_score: null, @@ -66,54 +62,32 @@ export const mockResponse = { }, aggregations: { unique_source_ips: { - value: 7600, + value: 10503, }, - host: { - value: 6, + hosts: { + value: 711, }, unique_destination_ips: { - value: 1946, + value: 2380, }, - sockets: { - value: 0, - }, - installedPackages: { - value: 0, + agents: { + value: 23, }, }, status: 200, }, { - took: 265, + took: 3884, timed_out: false, _shards: { - total: 47, - successful: 47, - skipped: 40, + total: 67, + successful: 67, + skipped: 60, failed: 0, }, hits: { total: { - value: 11, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, - }, - { - took: 243, - timed_out: false, - _shards: { - total: 47, - successful: 47, - skipped: 40, - failed: 0, - }, - hits: { - total: { - value: 27, + value: 661651, relation: 'eq', }, max_score: null, @@ -121,103 +95,87 @@ export const mockResponse = { }, aggregations: { authentication_success: { - doc_count: 27, + doc_count: 2, }, authentication_failure: { - doc_count: 0, + doc_count: 661649, }, }, status: 200, }, - { - took: 231, - timed_out: false, - _shards: { - total: 47, - successful: 47, - skipped: 40, - failed: 0, - }, - hits: { - total: { - value: 0, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, + ], +}; + +export const mockResult = { + hosts: 711, + agents: 23, + authentication: { + success: 2, + failure: 661649, + }, + uniqueSourceIps: 10503, + uniqueDestinationIps: 2380, +}; + +export const mockGeneralQuery = [ + { + index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + allowNoIndices: true, + ignoreUnavailable: true, + }, + { + aggregations: { + hosts: { cardinality: { field: 'host.name' } }, + agents: { cardinality: { field: 'agent.id' } }, + unique_source_ips: { cardinality: { field: 'source.ip' } }, + unique_destination_ips: { cardinality: { field: 'destination.ip' } }, }, - { - took: 273, - timed_out: false, - _shards: { - total: 47, - successful: 47, - skipped: 40, - failed: 0, - }, - hits: { - total: { - value: 0, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - status: 200, + query: { + bool: { filter: [{ range: { '@timestamp': { gte: 1549765606071, lte: 1549852006071 } } }] }, }, - { - took: 240, - timed_out: false, - _shards: { - total: 47, - successful: 47, - skipped: 40, - failed: 0, + 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' } }, + aggs: { attempts_over_time: { auto_date_histogram: { field: '@timestamp', buckets: 10 } } }, }, - hits: { - total: { - value: 8787, - relation: 'eq', - }, - max_score: null, - hits: [], + authentication_failure: { + filter: { term: { 'event.type': 'authentication_failure' } }, + aggs: { attempts_over_time: { auto_date_histogram: { field: '@timestamp', buckets: 10 } } }, }, - status: 200, }, - { - took: 231, - timed_out: false, - _shards: { - total: 47, - successful: 47, - skipped: 40, - failed: 0, - }, - hits: { - total: { - value: 956933, - relation: 'eq', - }, - max_score: null, - hits: [], + query: { + bool: { + filter: [ + { + bool: { + should: [ + { match: { 'event.type': 'authentication_success' } }, + { match: { 'event.type': 'authentication_failure' } }, + ], + minimum_should_match: 1, + }, + }, + { range: { '@timestamp': { gte: 1549765606071, lte: 1549852006071 } } }, + ], }, - status: 200, }, - ], -}; + size: 0, + track_total_hits: true, + }, +]; -export const mockResult = { - auditbeatEvents: 0, - authenticationFailure: 0, - authenticationAttempts: 27, - filebeatEvents: 956933, - hosts: 6, - installedPackages: 0, - processCount: 11, - sockets: 0, - uniqueDestinationIps: 1946, - uniqueSourceIps: 7600, - winlogbeatEvents: 8787, +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 index c05c5fec2f9ae..2c40e3ff39e23 100644 --- 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 @@ -66,6 +66,14 @@ export const buildAuthQuery = ({ 'event.type': 'authentication_success', }, }, + aggs: { + attempts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 10, + }, + }, + }, }, authentication_failure: { filter: { @@ -73,6 +81,14 @@ export const buildAuthQuery = ({ 'event.type': 'authentication_failure', }, }, + aggs: { + attempts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 10, + }, + }, + }, }, }, query: { @@ -81,7 +97,7 @@ export const buildAuthQuery = ({ }, }, size: 0, - track_total_hits: true, + track_total_hits: false, }, ]; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts deleted file mode 100644 index b801c259804b2..0000000000000 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts +++ /dev/null @@ -1,101 +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 { createQueryFilterClauses } from '../../utils/build_query'; -import { RequestBasicOptions } from '../framework'; - -import { EventModuleAttributeQuery, KpiHostsESMSearchBody } from './types'; - -const getAgentTypeFilter = ({ agentType }: EventModuleAttributeQuery) => - agentType - ? [ - { - bool: { - should: [ - { - match_phrase: { - 'agent.type': agentType, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ] - : []; - -const getEventModuleFilter = ({ agentType }: EventModuleAttributeQuery) => - agentType === 'auditbeat' - ? [ - { - bool: { - should: [ - { - match_phrase: { - 'event.module': 'file_integrity', - }, - }, - { - match_phrase: { - 'event.module': 'auditd', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ] - : []; - -const getEventQueryFilter = (attrQuery: EventModuleAttributeQuery) => [ - ...getAgentTypeFilter(attrQuery), - ...getEventModuleFilter(attrQuery), -]; -export const buildEventQuery = ( - attrQuery: EventModuleAttributeQuery, - { - filterQuery, - timerange: { from, to }, - sourceConfiguration: { - fields: { timestamp }, - logAlias, - auditbeatAlias, - packetbeatAlias, - winlogbeatAlias, - }, - }: RequestBasicOptions -): KpiHostsESMSearchBody[] => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - ...getEventQueryFilter(attrQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - }, - }, - }, - ]; - - const dslQuery = [ - { - index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], - allowNoIndices: true, - ignoreUnavailable: true, - }, - { - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: true, - }, - ]; - - 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 index ad69487c9a19a..fcffdec0edaa6 100644 --- 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 @@ -39,19 +39,14 @@ export const buildGeneralQuery = ({ }, { aggregations: { - host: { + hosts: { cardinality: { field: 'host.name', }, }, - installedPackages: { + agents: { cardinality: { - field: 'system.audit.package.entity_id', - }, - }, - sockets: { - cardinality: { - field: 'socket.entity_id', + field: 'agent.id', }, }, unique_source_ips: { @@ -71,7 +66,7 @@ export const buildGeneralQuery = ({ }, }, size: 0, - track_total_hits: true, + track_total_hits: false, }, ]; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts deleted file mode 100644 index 9905042094c28..0000000000000 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/query_process_count.dsl.ts +++ /dev/null @@ -1,78 +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 { createQueryFilterClauses } from '../../utils/build_query'; -import { RequestBasicOptions } from '../framework'; - -import { KpiHostsESMSearchBody } from './types'; - -const getProcessQueryFilter = () => [ - { - bool: { - should: [ - { - match: { - 'event.action': 'process_started', - }, - }, - { - match: { - 'event.action': 'executed', - }, - }, - { - match: { - 'event.code': 4688, - }, - }, - ], - minimum_should_match: 1, - }, - }, -]; - -export const buildProcessQuery = ({ - filterQuery, - timerange: { from, to }, - sourceConfiguration: { - fields: { timestamp }, - logAlias, - auditbeatAlias, - packetbeatAlias, - winlogbeatAlias, - }, -}: RequestBasicOptions): KpiHostsESMSearchBody[] => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - ...getProcessQueryFilter(), - { - range: { - [timestamp]: { - gte: from, - lte: to, - }, - }, - }, - ]; - - const dslQuery = [ - { - index: [logAlias, auditbeatAlias, packetbeatAlias, winlogbeatAlias], - allowNoIndices: true, - ignoreUnavailable: true, - }, - { - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: true, - }, - ]; - - 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 index a1894d9edef5d..db931f4efd066 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts @@ -13,19 +13,16 @@ export interface KpiHostsAdapter { export interface KpiHostsHit extends SearchHit { aggregations: { - host: { + hosts: { value: number; }; - installedPackages: { + agents: { value: number; }; - processCount: { + unique_source_ips: { value: number; }; - authenticationSuccess: { - value: number; - }; - authenticationFailure: { + unique_destination_ips: { value: number; }; }; diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 60d167609edc1..fbf868b9c22e5 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -36,13 +36,9 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { .then(resp => { const kpiHosts = resp.data.source.KpiHosts; expect(kpiHosts!.hosts).to.be(1); - expect(kpiHosts!.installedPackages).to.be(0); - expect(kpiHosts!.processCount).to.equal(0); - expect(kpiHosts!.authenticationAttempts).to.equal(0); - expect(kpiHosts!.auditbeatEvents).to.equal(0); - expect(kpiHosts!.winlogbeatEvents).to.equal(0); - expect(kpiHosts!.filebeatEvents).to.equal(6157); - expect(kpiHosts!.sockets).to.equal(0); + expect(kpiHosts!.agents).to.be(1); + expect(kpiHosts!.authentication!.success).to.equal(0); + expect(kpiHosts!.authentication!.failure).to.equal(0); expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); }); @@ -71,15 +67,10 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }) .then(resp => { const kpiHosts = resp.data.source.KpiHosts; - expect(kpiHosts!.hosts).to.be(1); - expect(kpiHosts!.installedPackages).to.be(0); - expect(kpiHosts!.processCount).to.equal(0); - expect(kpiHosts!.authenticationAttempts).to.equal(0); - expect(kpiHosts!.auditbeatEvents).to.equal(0); - expect(kpiHosts!.winlogbeatEvents).to.equal(0); - expect(kpiHosts!.filebeatEvents).to.equal(6157); - expect(kpiHosts!.sockets).to.equal(0); + expect(kpiHosts!.agents).to.be(1); + expect(kpiHosts!.authentication!.success).to.equal(0); + expect(kpiHosts!.authentication!.failure).to.equal(0); expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); }); @@ -88,5 +79,5 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }); }; -// tslint:disable-next-line no-default-export +// eslint-disable-next-line import/no-default-export export default kpiHostsTests; From d9878667f480775177eecdf3854732f3f6e4ec42 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Wed, 24 Apr 2019 20:32:37 +0800 Subject: [PATCH 03/26] add charts --- .../components/page/hosts/kpi_hosts/index.tsx | 149 ++++- .../page/hosts/kpi_hosts/translations.ts | 19 +- .../public/components/stat_items/index.tsx | 145 ++++- .../containers/kpi_hosts/index.gql_query.ts | 25 +- .../siem/public/graphql/introspection.json | 78 ++- x-pack/plugins/siem/public/graphql/types.ts | 76 ++- .../graphql/kpi_hosts/kpi_hosts.mock.ts | 87 ++- .../server/graphql/kpi_hosts/schema.gql.ts | 16 +- .../server/graphql/kpi_hosts/schema.test.ts | 25 +- x-pack/plugins/siem/server/graphql/types.ts | 106 +++- .../kpi_hosts/elasticsearch_adapter.test.ts | 12 +- .../lib/kpi_hosts/elasticsearch_adapter.ts | 12 +- .../plugins/siem/server/lib/kpi_hosts/mock.ts | 519 ++++++++++++++---- .../lib/kpi_hosts/query_authentication.dsl.ts | 4 +- .../server/lib/kpi_hosts/query_general.dsl.ts | 67 ++- 15 files changed, 1112 insertions(+), 228 deletions(-) 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 index 4c1c4f1b694ad..51ca1df151eb2 100644 --- 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 @@ -10,7 +10,7 @@ import React from 'react'; import { pure } from 'recompose'; import { KpiHostsData } from '../../../../graphql/types'; -import { StatItem, StatItems, StatItemsComponent } from '../../../stat_items'; +import { StatItem, StatItems, StatItemsComponent, AreaChartData, BarChartData, StatItemsProps } from '../../../stat_items'; import * as i18n from './translations'; @@ -19,42 +19,72 @@ interface KpiHostsProps { loading: boolean; } +const euiColorVis0 = '#00B3A4'; +const euiColorVis1 = '#3185FC'; +const euiColorVis2 = '#DB1374'; +const euiColorVis3 = '#490092'; +const euiColorVis9 = '#920000'; + const fieldTitleMapping: StatItems[] = [ { fields: [ { key: 'hosts', - description: i18n.HOSTS, value: null, + color: euiColorVis1, }, ], - }, - { - fields: [ + areaChart: [ { - key: 'agents', - description: i18n.AGENTS, + key: 'hostsHistogram', value: null, - }, + color: euiColorVis1, + } ], + grow: 2, + description: i18n.HOSTS, }, { fields: [ { - key: 'authentication.success', + key: 'authSuccess', description: i18n.AUTHENTICATION_SUCCESS, value: null, + color: euiColorVis0, }, - ], - }, - { - fields: [ { - key: 'authentication.failure', + key: 'authFailure', description: i18n.AUTHENTICATION_FAILURE, value: null, + color: euiColorVis9, + }, + ], + areaChart: [ + { + key: 'authSuccessHistogram', + value: null, + color: euiColorVis0, + }, + { + key: 'authFailureHistogram', + value: null, + color: euiColorVis9, + }, + ], + barChart: [ + { + key: 'authSuccess', + value: null, + color: euiColorVis0, + }, + { + key: 'authFailure', + value: null, + color: euiColorVis9, }, ], + grow: 4, + description: i18n.AUTHENTICATION, }, { fields: [ @@ -62,34 +92,101 @@ const fieldTitleMapping: StatItems[] = [ key: 'uniqueSourceIps', description: i18n.UNIQUE_SOURCE_IPS, value: null, + color: euiColorVis2, }, - ], - }, - { - fields: [ { key: 'uniqueDestinationIps', description: i18n.UNIQUE_DESTINATION_IPS, value: null, + color: euiColorVis3, + }, + ], + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: null, + color: euiColorVis2 + }, + { + key: 'uniqueDestinationIpsHistogram', + value: null, + color: euiColorVis3, + }, + ], + barChart: [ + { + key: 'uniqueSourceIps', + value: null, + color: euiColorVis2, + }, + { + key: 'uniqueDestinationIps', + value: null, + color: euiColorVis3, }, ], + grow: 4, + description: i18n.UNIQUE_PRIVATE_IPS, }, ]; export const KpiHostsComponent = pure(({ data, loading }) => { + return ( - {fieldTitleMapping.map(card => ( - - ))} + { + fieldTitleMapping.map(card => { + let statItemProps: StatItemsProps = { + ...card, + isLoading: loading, + key: `kpi-hosts-summary-${card.description}`, + } + + if (card.fields != null) + statItemProps = { + ...statItemProps, + fields: addValueToFields(card.fields, data) + } + + if (card.areaChart != null) + statItemProps = { + ...statItemProps, + areaChart: addValueToChart(card.areaChart, data) + } + + if (card.barChart != null) + statItemProps = { + ...statItemProps, + barChart: addValueToBarChart(card.barChart, data) + } + + return + + }) + } ); }); + const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); + +const addValueToChart = (fields: AreaChartData[], data: KpiHostsData): AreaChartData[] => + fields.filter((field) => get(field.key, data) != null) + .map(field => ({ ...field, value: get(field.key, data) })); + +const addValueToBarChart = (fields: BarChartData[], data:KpiHostsData): BarChartData[] => { + + return fields.filter((field) => get(field.key, data) != null) + .map(field => { + return { + ...field, + value: [{ + x: get(field.key, data), + y: field.key + }] + } + }) +} + 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 index 165023f2ad4d0..ac3bbea79c9c4 100644 --- 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 @@ -16,14 +16,21 @@ export const AGENTS = i18n.translate('xpack.siem.kpiHosts.source.agentsTitle', { export const AUTHENTICATION_SUCCESS = i18n.translate( 'xpack.siem.kpiHosts.source.authenticationSuccessTitle', { - defaultMessage: 'Authentication Success', + defaultMessage: 'Success', } ); export const AUTHENTICATION_FAILURE = i18n.translate( 'xpack.siem.kpiHosts.source.authenticationFailureTitle', { - defaultMessage: 'Authentication Failure', + defaultMessage: 'Fail', + } +); + +export const AUTHENTICATION = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationTitle', + { + defaultMessage: 'User Authentications', } ); @@ -31,13 +38,17 @@ export const ACTIVE_USERS = i18n.translate('xpack.siem.kpiHosts.source.activeUse defaultMessage: 'Active Users', }); +export const UNIQUE_PRIVATE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniquePrivateIpsTitle', { + defaultMessage: 'Unique Private IPs', +}); + export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueSourceIpsTitle', { - defaultMessage: 'Unique Source Ips', + defaultMessage: 'Source', }); export const UNIQUE_DESTINATION_IPS = i18n.translate( 'xpack.siem.kpiHosts.source.uniqueDestinationIpsTitle', { - defaultMessage: 'Unique Destination Ips', + defaultMessage: 'Dest.', } ); diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index 141e46fa4af9a..a360e05b6a269 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -9,24 +9,57 @@ import { EuiFlexItem, EuiLoadingSpinner, EuiPanel, + EuiHorizontalRule, + // @ts-ignore + EuiBarSeries, // @ts-ignore EuiStat, + // @ts-ignore + EuiSeriesChartUtils, + EuiIcon, } from '@elastic/eui'; + +const { SCALE, ORIENTATION } = EuiSeriesChartUtils; +const iconType = 'stopFilled'; +import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; import numeral from '@elastic/numeral'; import React from 'react'; import { pure } from 'recompose'; import { getEmptyTagValue } from '../empty_value'; +import { LoadingPanel } from '../loading'; export interface StatItem { key: string; - description: string; + description?: string; value: number | undefined | null; + color: string; +} + +export interface AreaChartData { + key: string; + value: ChartData[] | null; + color: string; +} + +export interface ChartData { + x: number; + y: number | string; + y0?: number; +} + +export interface BarChartData { + key: string; + value: ChartData[] | null; + color: string; } export interface StatItems { fields: StatItem[]; description?: string; + areaChart?: AreaChartData[]; + barChart?: BarChartData[]; + grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; } export interface StatItemsProps extends StatItems { @@ -36,46 +69,100 @@ export interface StatItemsProps extends StatItems { const CardTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( ({ isLoading, value }) => ( - <> + {isLoading ? ( - + ) : value != null ? ( numeral(value).format('0,0') ) : ( getEmptyTagValue() )} - + ) ); +const getChartSpan = (barChart: BarChartData[] | undefined | null, areaChart: AreaChartData[] | undefined | null) => { + return barChart && barChart.length && areaChart && areaChart.length ? 5 : 10 +} + export const StatItemsComponent = pure( - ({ fields, description, isLoading, key }) => ( - + ({ fields, description, isLoading, key, grow, barChart, areaChart }) => ( + - {fields.length === 1 ? ( - } - description={fields[0].description} - /> - ) : ( - ( - - - + + { + fields.map(field => ( + + + + + + + + + + {field.description} + + - - {field.description} - - - ))} - description={description} - /> - )} + )) + } + + }/> + { areaChart || barChart ? : null } + { isLoading && (areaChart || barChart) ? : ( + + { + barChart ? ( + + + { + barChart.map(series => series.value != null ? ( + + ) : null) + } + + + ) : null + } + { + areaChart ? ( + + + { + areaChart.map(series => series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + + ) : null) + } + + + ) : null + } + + + )} ) 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 index 3eafa21ff3f15..fc631e503a093 100644 --- 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 @@ -12,13 +12,30 @@ export const kpiHostsQuery = gql` id KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { hosts - agents - authentication { - success - failure + hostsHistogram { + x: key + y: doc_count + } + authSuccess + authSuccessHistogram { + x: key + y: doc_count + } + authFailure + authFailureHistogram { + x: key + y: doc_count } uniqueSourceIps + uniqueSourceIpsHistogram { + x: key + y: doc_count + } uniqueDestinationIps + uniqueDestinationIpsHistogram { + x: key + y: doc_count + } } } } diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index fdb08d3d44ac0..54bcf7dfde05c 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -6303,7 +6303,19 @@ "deprecationReason": null }, { - "name": "agents", + "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 }, @@ -6311,13 +6323,33 @@ "deprecationReason": null }, { - "name": "authentication", + "name": "authSuccessHistogram", "description": "", "args": [], "type": { - "kind": "NON_NULL", + "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": "authenticationData", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "HistogramData", "ofType": null } }, "isDeprecated": false, "deprecationReason": null @@ -6330,6 +6362,18 @@ "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": "", @@ -6337,6 +6381,18 @@ "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, @@ -6346,11 +6402,11 @@ }, { "kind": "OBJECT", - "name": "authenticationData", + "name": "HistogramData", "description": "", "fields": [ { - "name": "success", + "name": "key", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -6358,12 +6414,20 @@ "deprecationReason": null }, { - "name": "failure", + "name": "doc_count", "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 } ], "inputFields": null, diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 7d59f19ac81d3..9fd969a5b7798 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -1047,19 +1047,31 @@ export interface KpiNetworkData { export interface KpiHostsData { hosts?: number | null; - agents?: number | null; + hostsHistogram?: (HistogramData | null)[] | null; - authentication: AuthenticationData; + 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 AuthenticationData { - success?: number | null; +export interface HistogramData { + key?: number | null; + + doc_count?: number | null; - failure?: number | null; + key_as_string?: string | null; } export interface NetworkTopNFlowData { @@ -2410,21 +2422,63 @@ export namespace GetKpiHostsQuery { hosts?: number | null; - agents?: number | null; + hostsHistogram?: (HostsHistogram | null)[] | null; + + authSuccess?: number | null; - authentication: Authentication; + 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 = { + __typename?: 'HistogramData'; + + x?: string | null; + + y?: number | null; + }; + + export type AuthSuccessHistogram = { + __typename?: 'HistogramData'; + + x?: string | null; + + y?: number | null; + }; + + export type AuthFailureHistogram = { + __typename?: 'HistogramData'; + + x?: string | null; + + y?: number | null; + }; + + export type UniqueSourceIpsHistogram = { + __typename?: 'HistogramData'; + + x?: string | null; + + y?: number | null; }; - export type Authentication = { - __typename?: 'authenticationData'; + export type UniqueDestinationIpsHistogram = { + __typename?: 'HistogramData'; - success?: number | null; + x?: string | null; - failure?: number | null; + y?: number | null; }; } diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts index f25781ffb207f..da52d5720a302 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts @@ -10,14 +10,85 @@ import { KpiHostsData } from '../types'; export const mockKpiHostsData: { KpiHosts: KpiHostsData } = { KpiHosts: { - hosts: 0, - agents: 0, - authentication: { - success: 0, - failure: 0, - }, - uniqueSourceIps: 0, - uniqueDestinationIps: 0, + hosts: 1026, + hostsHistogram: [ + { + "doc_count": 5017216, + "key": 1556089200000, + }, + { + "doc_count": 4590090, + "key": 1556132400000, + }, + { + "doc_count": 73901, + "key": 1556175600000, + }, + ], + authSuccess: 2, + authSuccessHistogram: [ + { + "doc_count": 1, + "key": 1556128800000, + }, + { + "doc_count": 0, + "key": 1556139600000, + }, + { + "doc_count": 0, + "key": 1556150400000, + }, + { + "doc_count": 1, + "key": 1556161200000, + }, + ], + authFailure: 306495, + authFailureHistogram: [ + { + "doc_count": 220265, + "key": 1556089200000, + }, + { + "doc_count": 86135, + "key": 1556132400000, + }, + { + "doc_count": 95, + "key": 1556175600000, + }, + ], + uniqueSourceIps: 11929, + uniqueSourceIpsHistogram: [ + { + "doc_count": 1419836, + "key": 1556089200000, + }, + { + "doc_count": 1074440, + "key": 1556132400000, + }, + { + "doc_count": 9328, + "key": 1556175600000, + }, + ], + uniqueDestinationIps: 2662, + uniqueDestinationIpsHistogram: [ + { + "doc_count": 1189146, + "key": 1556089200000, + }, + { + "doc_count": 977334, + "key": 1556132400000, + }, + { + "doc_count": 8656, + "key": 1556175600000, + }, + ], }, }; 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 index 768182c3dd9df..de291c8f812c4 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts @@ -7,17 +7,23 @@ import gql from 'graphql-tag'; export const kpiHostsSchema = gql` - type authenticationData { - success: Float - failure: Float + type HistogramData { + key: Float + doc_count: Float + key_as_string: String } type KpiHostsData { hosts: Float - agents: Float - authentication: authenticationData! + hostsHistogram: [HistogramData] + authSuccess: Float + authSuccessHistogram: [HistogramData] + authFailure: Float + authFailureHistogram: [HistogramData] uniqueSourceIps: Float + uniqueSourceIpsHistogram: [HistogramData] uniqueDestinationIps: Float + uniqueDestinationIpsHistogram: [HistogramData] } extend type Source { diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts index 5412d24b33794..208ca8a3562ec 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts @@ -31,13 +31,30 @@ const testKpiHostsSource = { source(id: "default") { KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { hosts - agents - authentication { - success - failure + hostsHistogram { + key + doc_count + } + authSuccess + authSuccessHistogram { + key + doc_count + } + authFailure + authFailureHistogram { + key + doc_count } uniqueSourceIps + uniqueSourceIpsHistogram { + key + doc_count + } uniqueDestinationIps + uniqueDestinationIpsHistogram { + key + doc_count + } } } } diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index 5f8b91fab827a..51eeda7204dc2 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -1076,19 +1076,31 @@ export interface KpiNetworkData { export interface KpiHostsData { hosts?: number | null; - agents?: number | null; + hostsHistogram?: (HistogramData | null)[] | null; - authentication: AuthenticationData; + 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 AuthenticationData { - success?: number | null; +export interface HistogramData { + key?: number | null; + + doc_count?: number | null; - failure?: number | null; + key_as_string?: string | null; } export interface NetworkTopNFlowData { @@ -5186,13 +5198,39 @@ export namespace KpiHostsDataResolvers { export interface Resolvers { hosts?: HostsResolver; - agents?: AgentsResolver; + hostsHistogram?: HostsHistogramResolver<(HistogramData | null)[] | null, TypeParent, Context>; + + authSuccess?: AuthSuccessResolver; + + authSuccessHistogram?: AuthSuccessHistogramResolver< + (HistogramData | null)[] | null, + TypeParent, + Context + >; + + authFailure?: AuthFailureResolver; - authentication?: AuthenticationResolver; + 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< @@ -5200,13 +5238,28 @@ export namespace KpiHostsDataResolvers { Parent = KpiHostsData, Context = SiemContext > = Resolver; - export type AgentsResolver< + 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 AuthenticationResolver< - R = AuthenticationData, + export type AuthFailureHistogramResolver< + R = (HistogramData | null)[] | null, Parent = KpiHostsData, Context = SiemContext > = Resolver; @@ -5215,28 +5268,45 @@ export namespace KpiHostsDataResolvers { 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 AuthenticationDataResolvers { - export interface Resolvers { - success?: SuccessResolver; +export namespace HistogramDataResolvers { + export interface Resolvers { + key?: KeyResolver; + + doc_count?: DocCountResolver; - failure?: FailureResolver; + key_as_string?: KeyAsStringResolver; } - export type SuccessResolver< + export type KeyResolver< R = number | null, - Parent = AuthenticationData, + Parent = HistogramData, Context = SiemContext > = Resolver; - export type FailureResolver< + export type DocCountResolver< R = number | null, - Parent = AuthenticationData, + Parent = HistogramData, + Context = SiemContext + > = Resolver; + export type KeyAsStringResolver< + R = string | null, + Parent = HistogramData, Context = SiemContext > = Resolver; } 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 index ae5bbd0f3f077..fc413d350a589 100644 --- 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 @@ -106,13 +106,15 @@ describe('Hosts Kpi elasticsearch_adapter', () => { test('getKpiHosts - response without data', async () => { expect(data).toEqual({ hosts: null, - agents: null, - authentication: { - success: null, - failure: 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 index e20b40e7be7d4..2e898d13dfdd6 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -33,17 +33,19 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { return { hosts: getOr(null, 'responses.0.aggregations.hosts.value', response), - agents: getOr(null, 'responses.0.aggregations.agents.value', response), - authentication: { - success: getOr(null, 'responses.1.aggregations.authentication_success.doc_count', response), - failure: getOr(null, 'responses.1.aggregations.authentication_failure.doc_count', response), - }, + hostsHistogram: getOr(null, 'responses.0.aggregations.hosts_hostogram.hosts_over_time.buckets', response), + authSuccess: getOr(null, 'responses.1.aggregations.authentication_success.doc_count', response), + authSuccessHistogram: getOr(null, 'responses.1.aggregations.authentication_success.attempts_over_time.buckets', response), + authFailure: getOr(null, 'responses.1.aggregations.authentication_failure.doc_count', response), + authFailureHistogram: getOr(null, 'responses.1.aggregations.authentication_failure.attempts_over_time.buckets', response), uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response), + uniqueSourceIpsHistogram: getOr(null, 'responses.0.aggregations.unique_source_ips_hostogram.ips_over_time.buckets', response), uniqueDestinationIps: getOr( null, 'responses.0.aggregations.unique_destination_ips.value', response ), + uniqueDestinationIpsHistogram: getOr(null, 'responses.0.aggregations.unique_destination_ips_hostogram.ips_over_time.buckets', response), }; } } diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts index 5b401c383db3f..61222deb3596e 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -41,139 +41,464 @@ export const mockRequest = { }; export const mockResponse = { - took: 577, - responses: [ - { - took: 2603, - timed_out: false, - _shards: { - total: 67, - successful: 67, - skipped: 60, - failed: 0, + "took": 4405, + "responses": [ + { + "took": 4404, + "timed_out": false, + "_shards": { + "total": 71, + "successful": 71, + "skipped": 64, + "failed": 0 }, - hits: { - total: { - value: 9665113, - relation: 'eq', - }, - max_score: null, - hits: [], + "hits": { + "max_score": null, + "hits": [] }, - aggregations: { - unique_source_ips: { - value: 10503, + "aggregations": { + "unique_destination_ips_hostogram": { + "doc_count": 2175136, + "ips_over_time": { + "buckets": [ + { + "key_as_string": "2019-04-24T07:00:00.000Z", + "key": 1556089200000, + "doc_count": 1189146 + }, + { + "key_as_string": "2019-04-24T19:00:00.000Z", + "key": 1556132400000, + "doc_count": 977334 + }, + { + "key_as_string": "2019-04-25T07:00:00.000Z", + "key": 1556175600000, + "doc_count": 8656 + } + ], + "interval": "12h" + } }, - hosts: { - value: 711, + "unique_source_ips": { + "value": 11929 }, - unique_destination_ips: { - value: 2380, + "hosts": { + "value": 1026 }, - agents: { - value: 23, + "hosts_hostogram": { + "doc_count": 9681207, + "hosts_over_time": { + "buckets": [ + { + "key_as_string": "2019-04-24T07:00:00.000Z", + "key": 1556089200000, + "doc_count": 5017216 + }, + { + "key_as_string": "2019-04-24T19:00:00.000Z", + "key": 1556132400000, + "doc_count": 4590090 + }, + { + "key_as_string": "2019-04-25T07:00:00.000Z", + "key": 1556175600000, + "doc_count": 73901 + } + ], + "interval": "12h" + } }, + "unique_destination_ips": { + "value": 2662 + }, + "unique_source_ips_hostogram": { + "doc_count": 2503604, + "ips_over_time": { + "buckets": [ + { + "key_as_string": "2019-04-24T07:00:00.000Z", + "key": 1556089200000, + "doc_count": 1419836 + }, + { + "key_as_string": "2019-04-24T19:00:00.000Z", + "key": 1556132400000, + "doc_count": 1074440 + }, + { + "key_as_string": "2019-04-25T07:00:00.000Z", + "key": 1556175600000, + "doc_count": 9328 + } + ], + "interval": "12h" + } + } }, - status: 200, + "status": 200 }, { - took: 3884, - timed_out: false, - _shards: { - total: 67, - successful: 67, - skipped: 60, - failed: 0, + "took": 1124, + "timed_out": false, + "_shards": { + "total": 71, + "successful": 71, + "skipped": 64, + "failed": 0 }, - hits: { - total: { - value: 661651, - relation: 'eq', - }, - max_score: null, - hits: [], + "hits": { + "max_score": null, + "hits": [] }, - aggregations: { - authentication_success: { - doc_count: 2, - }, - authentication_failure: { - doc_count: 661649, + "aggregations": { + "authentication_success": { + "doc_count": 2, + "attempts_over_time": { + "buckets": [ + { + "key_as_string": "2019-04-24T18:00:00.000Z", + "key": 1556128800000, + "doc_count": 1 + }, + { + "key_as_string": "2019-04-24T21:00:00.000Z", + "key": 1556139600000, + "doc_count": 0 + }, + { + "key_as_string": "2019-04-25T00:00:00.000Z", + "key": 1556150400000, + "doc_count": 0 + }, + { + "key_as_string": "2019-04-25T03:00:00.000Z", + "key": 1556161200000, + "doc_count": 1 + } + ], + "interval": "3h" + } }, + "authentication_failure": { + "doc_count": 306495, + "attempts_over_time": { + "buckets": [ + { + "key_as_string": "2019-04-24T07:00:00.000Z", + "key": 1556089200000, + "doc_count": 220265 + }, + { + "key_as_string": "2019-04-24T19:00:00.000Z", + "key": 1556132400000, + "doc_count": 86135 + }, + { + "key_as_string": "2019-04-25T07:00:00.000Z", + "key": 1556175600000, + "doc_count": 95 + } + ], + "interval": "12h" + } + } }, - status: 200, - }, - ], + "status": 200 + } + ] }; export const mockResult = { - hosts: 711, - agents: 23, - authentication: { - success: 2, - failure: 661649, - }, - uniqueSourceIps: 10503, - uniqueDestinationIps: 2380, + hosts: 1026, + hostsHistogram: [ + { + "doc_count": 5017216, + "key": 1556089200000, + "key_as_string": "2019-04-24T07:00:00.000Z", + }, + { + "doc_count": 4590090, + "key": 1556132400000, + "key_as_string": "2019-04-24T19:00:00.000Z", + }, + { + "doc_count": 73901, + "key": 1556175600000, + "key_as_string": "2019-04-25T07:00:00.000Z", + }, + ], + authSuccess: 2, + authSuccessHistogram: [ + { + "doc_count": 1, + "key": 1556128800000, + "key_as_string": "2019-04-24T18:00:00.000Z", + }, + { + "doc_count": 0, + "key": 1556139600000, + "key_as_string": "2019-04-24T21:00:00.000Z", + }, + { + "doc_count": 0, + "key": 1556150400000, + "key_as_string": "2019-04-25T00:00:00.000Z", + }, + { + "doc_count": 1, + "key": 1556161200000, + "key_as_string": "2019-04-25T03:00:00.000Z", + }, + ], + authFailure: 306495, + authFailureHistogram: [ + { + "doc_count": 220265, + "key": 1556089200000, + "key_as_string": "2019-04-24T07:00:00.000Z", + }, + { + "doc_count": 86135, + "key": 1556132400000, + "key_as_string": "2019-04-24T19:00:00.000Z", + }, + { + "doc_count": 95, + "key": 1556175600000, + "key_as_string": "2019-04-25T07:00:00.000Z", + }, + ], + uniqueSourceIps: 11929, + uniqueSourceIpsHistogram: [ + { + "doc_count": 1419836, + "key": 1556089200000, + "key_as_string": "2019-04-24T07:00:00.000Z", + }, + { + "doc_count": 1074440, + "key": 1556132400000, + "key_as_string": "2019-04-24T19:00:00.000Z", + }, + { + "doc_count": 9328, + "key": 1556175600000, + "key_as_string": "2019-04-25T07:00:00.000Z", + }, + ], + uniqueDestinationIps: 2662, + uniqueDestinationIpsHistogram: [ + { + "doc_count": 1189146, + "key": 1556089200000, + "key_as_string": "2019-04-24T07:00:00.000Z", + }, + { + "doc_count": 977334, + "key": 1556132400000, + "key_as_string": "2019-04-24T19:00:00.000Z", + }, + { + "doc_count": 8656, + "key": 1556175600000, + "key_as_string": "2019-04-25T07:00:00.000Z", + }, + ], }; export const mockGeneralQuery = [ { - index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], - allowNoIndices: true, - ignoreUnavailable: true, + "index": [ + "filebeat-*", + "auditbeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "allowNoIndices": true, + "ignoreUnavailable": true }, { - aggregations: { - hosts: { cardinality: { field: 'host.name' } }, - agents: { cardinality: { field: 'agent.id' } }, - unique_source_ips: { cardinality: { field: 'source.ip' } }, - unique_destination_ips: { cardinality: { field: 'destination.ip' } }, + "aggregations": { + "hosts": { + "cardinality": { + "field": "host.name" + } + }, + "hosts_hostogram": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "host.name" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggregations": { + "hosts_over_time": { + "auto_date_histogram": { + "field": "@timestamp", + "buckets": "6" + } + } + } + }, + "unique_source_ips": { + "cardinality": { + "field": "source.ip" + } + }, + "unique_source_ips_hostogram": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "source.ip" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggregations": { + "ips_over_time": { + "auto_date_histogram": { + "field": "@timestamp", + "buckets": 6 + } + } + } + }, + "unique_destination_ips": { + "cardinality": { + "field": "destination.ip" + } + }, + "unique_destination_ips_hostogram": { + "filter": { + "bool": { + "should": [ + { + "exists": { + "field": "destination.ip" + } + } + ], + "minimum_should_match": 1 + } + }, + "aggregations": { + "ips_over_time": { + "auto_date_histogram": { + "field": "@timestamp", + "buckets": 6 + } + } + } + } }, - query: { - bool: { filter: [{ range: { '@timestamp': { gte: 1549765606071, lte: 1549852006071 } } }] }, + "query": { + "bool": { + "filter": [ + { + "range": { + "@timestamp": { + "gte": 1556091284295, + "lte": 1556177684295 + } + } + } + ] + } }, - size: 0, - track_total_hits: false, - }, + "size": 0, + "track_total_hits": false + } ]; export const mockAuthQuery = [ { - index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], - allowNoIndices: true, - ignoreUnavailable: true, + "index": [ + "filebeat-*", + "auditbeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "allowNoIndices": true, + "ignoreUnavailable": true }, { - aggs: { - authentication_success: { - filter: { term: { 'event.type': 'authentication_success' } }, - aggs: { attempts_over_time: { auto_date_histogram: { field: '@timestamp', buckets: 10 } } }, - }, - authentication_failure: { - filter: { term: { 'event.type': 'authentication_failure' } }, - aggs: { attempts_over_time: { auto_date_histogram: { field: '@timestamp', buckets: 10 } } }, + "aggs": { + "authentication_success": { + "filter": { + "term": { + "event.type": "authentication_success" + } + }, + "aggs": { + "attempts_over_time": { + "auto_date_histogram": { + "field": "@timestamp", + "buckets": 6 + } + } + } }, + "authentication_failure": { + "filter": { + "term": { + "event.type": "authentication_failure" + } + }, + "aggs": { + "attempts_over_time": { + "auto_date_histogram": { + "field": "@timestamp", + "buckets": 6 + } + } + } + } }, - query: { - bool: { - filter: [ + "query": { + "bool": { + "filter": [ { - bool: { - should: [ - { match: { 'event.type': 'authentication_success' } }, - { match: { 'event.type': 'authentication_failure' } }, + "bool": { + "should": [ + { + "match": { + "event.type": "authentication_success" + } + }, + { + "match": { + "event.type": "authentication_failure" + } + } ], - minimum_should_match: 1, - }, + "minimum_should_match": 1 + } }, - { range: { '@timestamp': { gte: 1549765606071, lte: 1549852006071 } } }, - ], - }, + { + "range": { + "@timestamp": { + "gte": 1556091284295, + "lte": 1556177684295 + } + } + } + ] + } }, - size: 0, - track_total_hits: true, - }, + "size": 0, + "track_total_hits": false + } ]; export const mockMsearchOptions = { 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 index 2c40e3ff39e23..4b6effcf7aa58 100644 --- 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 @@ -70,7 +70,7 @@ export const buildAuthQuery = ({ attempts_over_time: { auto_date_histogram: { field: '@timestamp', - buckets: 10, + buckets: 6, }, }, }, @@ -85,7 +85,7 @@ export const buildAuthQuery = ({ attempts_over_time: { auto_date_histogram: { field: '@timestamp', - buckets: 10, + buckets: 6, }, }, }, 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 index fcffdec0edaa6..7cea50db5c814 100644 --- 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 @@ -44,9 +44,26 @@ export const buildGeneralQuery = ({ field: 'host.name', }, }, - agents: { - cardinality: { - field: 'agent.id', + hosts_hostogram: { + filter: { + bool: { + should: [ + { + exists: { + field: 'host.name' + } + } + ], + minimum_should_match: 1 + } + }, + aggregations: { + hosts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6' + }, + }, }, }, unique_source_ips: { @@ -54,11 +71,55 @@ export const buildGeneralQuery = ({ field: 'source.ip', }, }, + unique_source_ips_hostogram: { + filter: { + bool: { + should: [ + { + exists: { + field: 'source.ip' + } + } + ], + minimum_should_match: 1 + } + }, + aggregations: { + ips_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6 + }, + }, + }, + }, unique_destination_ips: { cardinality: { field: 'destination.ip', }, }, + unique_destination_ips_hostogram: { + filter: { + bool: { + should: [ + { + exists: { + field: 'destination.ip' + } + } + ], + minimum_should_match: 1 + } + }, + aggregations: { + ips_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6 + }, + }, + }, + }, }, query: { bool: { From 1e103e139cb2a22ab997c774fb8a9fc3fc26177a Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Fri, 26 Apr 2019 14:09:02 +0800 Subject: [PATCH 04/26] isolate charts --- .../__snapshots__/index.test.tsx.snap | 840 +++++++++++++++++- .../components/stat_items/areachart.tsx | 22 + .../public/components/stat_items/barchart.tsx | 22 + .../components/stat_items/index.test.tsx | 110 ++- .../public/components/stat_items/index.tsx | 54 +- .../api_integration/apis/siem/kpi_hosts.ts | 147 ++- 6 files changed, 1108 insertions(+), 87 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/stat_items/areachart.tsx create mode 100644 x-pack/plugins/siem/public/components/stat_items/barchart.tsx 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 index f8a07e6aa01f6..771ed33c4d21d 100644 --- 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 @@ -1,10 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Stat Items rendering it renders loading icons 1`] = ` +exports[`Stat Items loading it renders loading icons 1`] = ` + + +
+ +
+ + + UNIQUE_PRIVATE_IPS + + + } + reverse={false} + textAlign="left" + title={ + + + + + + + + + + + UNIQUE_SOURCE_PRIVATE_IPS + + + + + + + + + + + + + UNIQUE_DESTINATION_PRIVATE_IPS + + + + + } + titleColor="default" + titleSize="m" + > +
+ +
+

+ + + UNIQUE_PRIVATE_IPS + + +

+
+
+ +

+ + + + + + + + + + + + + + + + + + + + + + + -- + + + + + + + + UNIQUE_SOURCE_PRIVATE_IPS + + + + + + + + + + + + + + + + + + + + + + + + + + + -- + + + + + + + + UNIQUE_DESTINATION_PRIVATE_IPS + + + + + + + + +

+
+
+
+ +
+
+ +
+ +
+ + + +
+ + } + className="euiSeriesChartContainer__emptyPrompt" + iconColor="subdued" + iconType="visualizeApp" + title={ + + Chart not available + + } + > +
+ + + + + + + + + + +
+ + + + + + Chart not available + + + +
+ + +
+

+

+
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + } + className="euiSeriesChartContainer__emptyPrompt" + iconColor="subdued" + iconType="visualizeApp" + title={ + + Chart not available + + } + > +
+ + + + + + + + + + +
+ + + + + + Chart not available + + + +
+ + +
+

+

+
+ + +
+ + +
+ + + +
+ +
+ +
+ +
+ +
+
+`; + +exports[`Stat Items rendering should render barChart 1`] = ` (({areaChart}) => ( + + { + areaChart.map(series => series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + + ) : null) + } + +)); 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..2316b261c45e9 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { + // @ts-ignore + EuiSeriesChartUtils +} from '@elastic/eui'; +import { pure } from 'recompose'; +import { BarChartData } from '.'; +import { EuiSeriesChart, EuiBarSeries } from '@elastic/eui/lib/experimental'; +const { SCALE, ORIENTATION } = EuiSeriesChartUtils; + +export const BarChart = pure<{barChart: BarChartData[]}>(({barChart}) => ( + + { + barChart.map(series => series.value != null ? ( + + ) : null) + } + +)); 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 index c8872a67a4ff8..37a84d3e5440e 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiStat, -} from '@elastic/eui'; -import { mount, shallow } from 'enzyme'; +import { mount, shallow, 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('rendering', () => { + describe('loading', () => { test('it renders loading icons', () => { const mockStatItemsData: StatItemsProps = { fields: [ @@ -23,6 +22,7 @@ describe('Stat Items', () => { key: 'networkEvents', description: 'NETWORK_EVENTS', value: null, + color: '#000000' }, ], isLoading: true, @@ -32,42 +32,76 @@ describe('Stat Items', () => { expect(toJson(wrapper)).toMatchSnapshot(); }); + }); + + describe('rendering', () => { + const mockStatItemsData: StatItemsProps = { + fields: [ + { + key: 'uniqueSourcePrivateIps', + description: 'UNIQUE_SOURCE_PRIVATE_IPS', + value: null, + color: '#000000', + }, + { + key: 'uniqueDestinationPrivateIps', + description: 'UNIQUE_DESTINATION_PRIVATE_IPS', + value: null, + color: '#000000', + }, + ], + areaChart: [ + { + key: 'uniqueSourcePrivateIps', + value: null, + color: '#000000', + }, + { + key: 'uniqueDestinationPrivateIps', + value: null, + color: '#000000', + }, + ], + barChart: [ + { + key: 'uniqueSourcePrivateIps', + value: null, + color: '#000000', + }, + { + key: 'uniqueDestinationPrivateIps', + value: null, + color: '#000000', + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + isLoading: false, + key: 'mock-keys', + }; + let wrapper: ReactWrapper; + beforeAll(() => { + + wrapper = mount(); + }) test('it renders the default widget', () => { - const mockStatItemsData: StatItemsProps = { - 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 mockStatItemsData: StatItemsProps = { - 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(EuiStat).prop('title')).toHaveLength(2); + test('should handle multiple titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).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 spliter', () => { + 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 index a360e05b6a269..ef93fe9de3a37 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -11,23 +11,20 @@ import { EuiPanel, EuiHorizontalRule, // @ts-ignore - EuiBarSeries, - // @ts-ignore EuiStat, - // @ts-ignore - EuiSeriesChartUtils, EuiIcon, } from '@elastic/eui'; -const { SCALE, ORIENTATION } = EuiSeriesChartUtils; const iconType = 'stopFilled'; -import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; import numeral from '@elastic/numeral'; import React from 'react'; import { pure } from 'recompose'; import { getEmptyTagValue } from '../empty_value'; import { LoadingPanel } from '../loading'; +import { BarChart } from './barchart'; +import { AreaChart } from './areachart'; +import { EuiTitle } from '@elastic/eui'; export interface StatItem { key: string; @@ -67,7 +64,7 @@ export interface StatItemsProps extends StatItems { key: string; } -const CardTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( +const StatTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( ({ isLoading, value }) => ( {isLoading ? ( @@ -87,9 +84,13 @@ const getChartSpan = (barChart: BarChartData[] | undefined | null, areaChart: Ar export const StatItemsComponent = pure( ({ fields, description, isLoading, key, grow, barChart, areaChart }) => ( - + - {description} + } + titleSize="m" + title={ { fields.map(field => ( @@ -99,15 +100,16 @@ export const StatItemsComponent = pure( component="span" gutterSize="m" alignItems="center" + data-test-subj="stat-title" > - + - {field.description} + {field.description} @@ -120,47 +122,23 @@ export const StatItemsComponent = pure( height="auto" width="100%" text="Loading" - data-test-subj="InitialLoadingPanelLoadMoreTable" + data-test-subj="InitialLoadingKpisHostsChart" /> : ( { barChart ? ( - - { - barChart.map(series => series.value != null ? ( - - ) : null) - } - + ) : null } { areaChart ? ( - - { - areaChart.map(series => series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ - // @ts-ignore - - ) : null) - } - + ) : null } - )} diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index fbf868b9c22e5..1d9dc502cd48a 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -34,13 +34,79 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }, }) .then(resp => { + const kpiHosts = resp.data.source.KpiHosts; - expect(kpiHosts!.hosts).to.be(1); - expect(kpiHosts!.agents).to.be(1); - expect(kpiHosts!.authentication!.success).to.equal(0); - expect(kpiHosts!.authentication!.failure).to.equal(0); + expect(kpiHosts!.hosts).to.equal(1); + expect(kpiHosts!.hostsHistogram).to.eql([{ + "x": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__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": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__typename": "HistogramData" + } + ]); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); + expect(kpiHosts!.uniqueDestinationIpsHistogram).to.eql([ + { + "x": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__typename": "HistogramData" + } + ]); }); }); }); @@ -67,12 +133,77 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }) .then(resp => { const kpiHosts = resp.data.source.KpiHosts; - expect(kpiHosts!.hosts).to.be(1); - expect(kpiHosts!.agents).to.be(1); - expect(kpiHosts!.authentication!.success).to.equal(0); - expect(kpiHosts!.authentication!.failure).to.equal(0); + expect(kpiHosts!.hosts).to.equal(1); + expect(kpiHosts!.hostsHistogram).to.eql([{ + "x": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__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": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__typename": "HistogramData" + } + ]); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); + expect(kpiHosts!.uniqueDestinationIpsHistogram).to.eql([ + { + "x": 1549728000000, + "y": 1574, + "__typename": "HistogramData" + }, + { + "x": 1549738800000, + "y": 0, + "__typename": "HistogramData" + }, + { + "x": 1549749600000, + "y": 1302, + "__typename": "HistogramData" + }, + { + "x": 1549760400000, + "y": 3281, + "__typename": "HistogramData" + } + ]); }); }); }); From a615768b01df1d680a29b7089ccd9fe0d02d235e Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 29 Apr 2019 14:49:38 +0800 Subject: [PATCH 05/26] fix app crashes on screen resizing --- .../components/stat_items/areachart.tsx | 38 +++++++++++----- .../public/components/stat_items/barchart.tsx | 45 ++++++++++++++----- .../public/components/stat_items/index.tsx | 28 +++++++++--- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index d11b9aa5f1dd4..4e8ffa2f5d15e 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -1,12 +1,15 @@ import React from 'react'; import { pure } from 'recompose'; -import { AreaChartData } from '.'; +import { AreaChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; +import { AutoSizer } from '../auto_sizer'; -export const AreaChart = pure<{areaChart: AreaChartData[]}>(({areaChart}) => ( - - { - areaChart.map(series => series.value != null ? ( + +const ChartBaseComponent = pure<{ data: AreaChartData[], width: number | undefined, height: number | undefined }>( + ({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( + + { + data.map(series => series.value != null ? ( /** * Placing ts-ignore here for fillOpacity * */ @@ -15,8 +18,23 @@ export const AreaChart = pure<{areaChart: AreaChartData[]}>(({areaChart}) => ( name={`stat-items-areachart-${series.key}`} data={series.value} fillOpacity={0.04} - color={series.color} /> - ) : null) - } - -)); + color={series.color} + /> + ) : null)} + + ) : null ); + + +export const AreaChart = pure<{areaChart: AreaChartData[]}>( + ({ areaChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + + )} + + ) +); + diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 2316b261c45e9..1ccabdcdeda7a 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -4,19 +4,40 @@ import { EuiSeriesChartUtils } from '@elastic/eui'; import { pure } from 'recompose'; -import { BarChartData } from '.'; +import { BarChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { EuiSeriesChart, EuiBarSeries } from '@elastic/eui/lib/experimental'; +import { AutoSizer } from '../auto_sizer'; const { SCALE, ORIENTATION } = EuiSeriesChartUtils; -export const BarChart = pure<{barChart: BarChartData[]}>(({barChart}) => ( - - { - barChart.map(series => series.value != null ? ( - ( + ({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( + + { + data.map(series => series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + - ) : null) - } - -)); + color={series.color} + /> + ) : null)} + + ) : null ); + + +export const BarChart = pure<{barChart: BarChartData[]}>( + ({ barChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + + )} + + ) +); \ No newline at end of file diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index ef93fe9de3a37..e697f00506e39 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -25,6 +25,20 @@ import { LoadingPanel } from '../loading'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; import { EuiTitle } from '@elastic/eui'; +import styled from 'styled-components'; + +export const WrappedByAutoSizer = styled.div` + height: 100px; + position: relative; +`; + +export const ChartOverlay = styled.div` + height: 100%; + width:100%; + top: 0; + left: 0; + position: absolute; +`; export interface StatItem { key: string; @@ -66,7 +80,7 @@ export interface StatItemsProps extends StatItems { const StatTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( ({ isLoading, value }) => ( - + {isLoading ? ( ) : value != null ? ( @@ -103,10 +117,14 @@ export const StatItemsComponent = pure( data-test-subj="stat-title" > - - - - + + + + {field.description} From e670e43d435559622db652fb113fad7d0f82a6f5 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 29 Apr 2019 21:50:47 +0800 Subject: [PATCH 06/26] gen types --- x-pack/plugins/siem/public/graphql/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 9fd969a5b7798..4040ccd25710f 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -2444,7 +2444,7 @@ export namespace GetKpiHostsQuery { export type HostsHistogram = { __typename?: 'HistogramData'; - x?: string | null; + x?: number | null; y?: number | null; }; @@ -2452,7 +2452,7 @@ export namespace GetKpiHostsQuery { export type AuthSuccessHistogram = { __typename?: 'HistogramData'; - x?: string | null; + x?: number | null; y?: number | null; }; @@ -2460,7 +2460,7 @@ export namespace GetKpiHostsQuery { export type AuthFailureHistogram = { __typename?: 'HistogramData'; - x?: string | null; + x?: number | null; y?: number | null; }; @@ -2468,7 +2468,7 @@ export namespace GetKpiHostsQuery { export type UniqueSourceIpsHistogram = { __typename?: 'HistogramData'; - x?: string | null; + x?: number | null; y?: number | null; }; @@ -2476,7 +2476,7 @@ export namespace GetKpiHostsQuery { export type UniqueDestinationIpsHistogram = { __typename?: 'HistogramData'; - x?: string | null; + x?: number | null; y?: number | null; }; From 83e473cfb88b8b8f99672643b68f43438cbf4f72 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 30 Apr 2019 01:33:29 +0800 Subject: [PATCH 07/26] fix lint error fix type error fix unit itest --- .../components/page/hosts/kpi_hosts/index.tsx | 90 +- .../page/hosts/kpi_hosts/translations.ts | 18 +- .../page/network/kpi_network/index.tsx | 14 +- .../__snapshots__/index.test.tsx.snap | 927 ++++++++++-------- .../components/stat_items/areachart.tsx | 72 +- .../public/components/stat_items/barchart.tsx | 78 +- .../components/stat_items/index.test.tsx | 59 +- .../public/components/stat_items/index.tsx | 93 +- .../graphql/kpi_hosts/kpi_hosts.mock.ts | 64 +- .../lib/kpi_hosts/elasticsearch_adapter.ts | 42 +- .../plugins/siem/server/lib/kpi_hosts/mock.ts | 630 ++++++------ .../server/lib/kpi_hosts/query_general.dsl.ts | 42 +- .../api_integration/apis/siem/kpi_hosts.ts | 165 ++-- 13 files changed, 1243 insertions(+), 1051 deletions(-) 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 index 51ca1df151eb2..cc9d864e3a161 100644 --- 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 @@ -8,10 +8,15 @@ import { EuiFlexGroup } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; - import { KpiHostsData } from '../../../../graphql/types'; -import { StatItem, StatItems, StatItemsComponent, AreaChartData, BarChartData, StatItemsProps } from '../../../stat_items'; - +import { + AreaChartData, + BarChartData, + StatItem, + StatItems, + StatItemsComponent, + StatItemsProps, +} from '../../../stat_items'; import * as i18n from './translations'; interface KpiHostsProps { @@ -39,7 +44,7 @@ const fieldTitleMapping: StatItems[] = [ key: 'hostsHistogram', value: null, color: euiColorVis1, - } + }, ], grow: 2, description: i18n.HOSTS, @@ -105,7 +110,7 @@ const fieldTitleMapping: StatItems[] = [ { key: 'uniqueSourceIpsHistogram', value: null, - color: euiColorVis2 + color: euiColorVis2, }, { key: 'uniqueDestinationIpsHistogram', @@ -131,62 +136,59 @@ const fieldTitleMapping: StatItems[] = [ ]; export const KpiHostsComponent = pure(({ data, loading }) => { - return ( - { - fieldTitleMapping.map(card => { - let statItemProps: StatItemsProps = { - ...card, - isLoading: loading, - key: `kpi-hosts-summary-${card.description}`, - } - - if (card.fields != null) + {fieldTitleMapping.map(card => { + let statItemProps: StatItemsProps = { + ...card, + isLoading: loading, + key: `kpi-hosts-summary-${card.description}`, + }; + + if (card.fields != null) statItemProps = { ...statItemProps, - fields: addValueToFields(card.fields, data) - } - - if (card.areaChart != null) - statItemProps = { - ...statItemProps, - areaChart: addValueToChart(card.areaChart, data) - } + fields: addValueToFields(card.fields, data), + }; - if (card.barChart != null) - statItemProps = { - ...statItemProps, - barChart: addValueToBarChart(card.barChart, data) - } + if (card.areaChart != null) + statItemProps = { + ...statItemProps, + areaChart: addValueToChart(card.areaChart, data), + }; - return + if (card.barChart != null) + statItemProps = { + ...statItemProps, + barChart: addValueToBarChart(card.barChart, data), + }; - }) - } + return ; + })} ); }); - const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); const addValueToChart = (fields: AreaChartData[], data: KpiHostsData): AreaChartData[] => - fields.filter((field) => get(field.key, data) != null) + fields + .filter(field => get(field.key, data) != null) .map(field => ({ ...field, value: get(field.key, data) })); -const addValueToBarChart = (fields: BarChartData[], data:KpiHostsData): BarChartData[] => { - - return fields.filter((field) => get(field.key, data) != null) +const addValueToBarChart = (fields: BarChartData[], data: KpiHostsData): BarChartData[] => { + return fields + .filter(field => get(field.key, data) != null) .map(field => { return { ...field, - value: [{ - x: get(field.key, data), - y: field.key - }] - } - }) -} - + value: [ + { + x: get(field.key, data), + y: field.key, + }, + ], + }; + }); +}; 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 index ac3bbea79c9c4..e6e4e5933eea4 100644 --- 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 @@ -27,20 +27,20 @@ export const AUTHENTICATION_FAILURE = i18n.translate( } ); -export const AUTHENTICATION = i18n.translate( - 'xpack.siem.kpiHosts.source.authenticationTitle', - { - defaultMessage: 'User Authentications', - } -); +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_PRIVATE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniquePrivateIpsTitle', { - defaultMessage: 'Unique Private IPs', -}); +export const UNIQUE_PRIVATE_IPS = i18n.translate( + 'xpack.siem.kpiHosts.source.uniquePrivateIpsTitle', + { + defaultMessage: 'Unique Private IPs', + } +); export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueSourceIpsTitle', { defaultMessage: 'Source', 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 96a8040d8606c..af564a2e1d44d 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 @@ -24,64 +24,64 @@ 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, }, ]; 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 index 771ed33c4d21d..6680f5d969ddb 100644 --- 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 @@ -16,7 +16,7 @@ exports[`Stat Items loading it renders loading icons 1`] = ` /> `; -exports[`Stat Items rendering it renders the default widget 1`] = ` +exports[`Stat Items rendering kpis with charts it renders the default widget 1`] = ` @@ -157,19 +158,29 @@ exports[`Stat Items rendering it renders the default widget 1`] = ` component="span" grow={2} > - - - - + + + + + + + + @@ -192,19 +204,29 @@ exports[`Stat Items rendering it renders the default widget 1`] = ` component="span" grow={2} > - - - - + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - -- - - - + + + + + -- + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - -- - - - + + + + + -- + + + + + + + - -
- - } - className="euiSeriesChartContainer__emptyPrompt" - iconColor="subdued" - iconType="visualizeApp" - title={ - - Chart not available - + + + +
- - - - - - - - - - -
- - - - - - Chart not available - - - -
- - -
-

-

-
- - -
- - -
- + className="sc-ifAKCX kHhUkS" + /> + +
+
+
@@ -688,131 +672,59 @@ exports[`Stat Items rendering it renders the default widget 1`] = ` ] } > - -
- - } - className="euiSeriesChartContainer__emptyPrompt" - iconColor="subdued" - iconType="visualizeApp" - title={ - - Chart not available - + + + +
- - - - - - - - - - -
- - - - - - Chart not available - - - -
- - -
-

-

-
- - -
- - -
- + className="sc-ifAKCX kHhUkS" + /> + +
+
+
@@ -827,39 +739,226 @@ exports[`Stat Items rendering it renders the default widget 1`] = ` `; -exports[`Stat Items rendering should render barChart 1`] = ` - +> + + +
+ +
+ + + UNIQUE_PRIVATE_IPS + + + } + reverse={false} + textAlign="left" + title={ + + + + + + + + + + + + + + + } + titleColor="default" + titleSize="m" + > +
+ +
+

+ + + UNIQUE_PRIVATE_IPS + + +

+
+
+ +

+ + + + + + + + + + + + + + + + -- + + + + + + + + + + + + + + + + + + +

+
+
+
+ +
+ +
+ +
+ + + `; diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 4e8ffa2f5d15e..1e4a5ac5b442a 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -1,40 +1,48 @@ +/* + * 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 { AreaChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; +import { AreaChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { AutoSizer } from '../auto_sizer'; - -const ChartBaseComponent = pure<{ data: AreaChartData[], width: number | undefined, height: number | undefined }>( - ({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( - - { - data.map(series => series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ - // @ts-ignore - - ) : null)} - - ) : null ); - - -export const AreaChart = pure<{areaChart: AreaChartData[]}>( - ({ areaChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - +const ChartBaseComponent = pure<{ + data: AreaChartData[]; + width: number | undefined; + height: number | undefined; +}>(({ data, ...chartConfigs }) => + chartConfigs.width && chartConfigs.height ? ( + + {data.map(series => + series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + + ) : null )} - - ) + + ) : null ); +export const AreaChart = pure<{ areaChart: AreaChartData[] }>(({ areaChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + + )} + +)); diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 1ccabdcdeda7a..47a4eb6845bc0 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -1,43 +1,57 @@ +/* + * 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 + EuiSeriesChartUtils, } from '@elastic/eui'; import { pure } from 'recompose'; -import { BarChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { EuiSeriesChart, EuiBarSeries } from '@elastic/eui/lib/experimental'; +import { BarChartData, WrappedByAutoSizer, ChartOverlay } from '.'; import { AutoSizer } from '../auto_sizer'; const { SCALE, ORIENTATION } = EuiSeriesChartUtils; - -const ChartBaseComponent = pure<{ data: BarChartData[], width: number | undefined, height: number | undefined }>( - ({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( - - { - data.map(series => series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ - // @ts-ignore - - ) : null)} +const ChartBaseComponent = pure<{ + data: BarChartData[]; + width: number | undefined; + height: number | undefined; +}>(({ data, ...chartConfigs }) => + chartConfigs.width && chartConfigs.height ? ( + + {data.map(series => + series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + + ) : null + )} - ) : null ); - + ) : null +); -export const BarChart = pure<{barChart: BarChartData[]}>( - ({ barChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - - )} - - ) -); \ No newline at end of file +export const BarChart = pure<{ barChart: BarChartData[] }>(({ barChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + + )} + +)); 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 index 37a84d3e5440e..d5fe3a7b1e45f 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -11,7 +11,8 @@ import * as React from 'react'; import { StatItemsComponent, StatItemsProps } from '.'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; -import { EuiHorizontalRule } from '@elastic/eui'; +import { EuiHorizontalRule, EuiIcon } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; describe('Stat Items', () => { describe('loading', () => { @@ -22,7 +23,7 @@ describe('Stat Items', () => { key: 'networkEvents', description: 'NETWORK_EVENTS', value: null, - color: '#000000' + color: '#000000', }, ], isLoading: true, @@ -31,10 +32,50 @@ describe('Stat Items', () => { const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); + }); + describe('rendering kpis without charts', () => { + const mockStatItemsData: StatItemsProps = { + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: null, + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + isLoading: false, + 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"]').filter(EuiFlexGroup)).toHaveLength(1); + }); + + test('should not render color indicators', () => { + expect(wrapper.find(EuiIcon)).toHaveLength(0); + }); + + test('should render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(0); + }); + + test('should render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(0); + }); + + test('should render spliter', () => { + expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0); + }); }); - describe('rendering', () => { + describe('rendering kpis with charts', () => { const mockStatItemsData: StatItemsProps = { fields: [ { @@ -80,19 +121,21 @@ describe('Stat Items', () => { }; 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); + expect(wrapper.find('[data-test-subj="stat-title"]').filter(EuiFlexGroup)).toHaveLength(2); + }); + + test('should render color indicators', () => { + expect(wrapper.find(EuiIcon)).toHaveLength(2); }); test('should render barChart', () => { - expect(wrapper.find(BarChart)).toHaveLength(1); }); @@ -100,7 +143,7 @@ describe('Stat Items', () => { expect(wrapper.find(AreaChart)).toHaveLength(1); }); - test('should render spliter', () => { + test('should render spliter', () => { 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 index e697f00506e39..e2fedc8151531 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -20,12 +20,12 @@ import numeral from '@elastic/numeral'; import React from 'react'; import { pure } from 'recompose'; +import { EuiTitle } from '@elastic/eui'; +import styled from 'styled-components'; import { getEmptyTagValue } from '../empty_value'; import { LoadingPanel } from '../loading'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; -import { EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; export const WrappedByAutoSizer = styled.div` height: 100px; @@ -34,7 +34,7 @@ export const WrappedByAutoSizer = styled.div` export const ChartOverlay = styled.div` height: 100%; - width:100%; + width: 100%; top: 0; left: 0; position: absolute; @@ -44,7 +44,7 @@ export interface StatItem { key: string; description?: string; value: number | undefined | null; - color: string; + color?: string; } export interface AreaChartData { @@ -82,7 +82,7 @@ const StatTitle = pure<{ isLoading: boolean; value: number | null | undefined }> ({ isLoading, value }) => ( {isLoading ? ( - + ) : value != null ? ( numeral(value).format('0,0') ) : ( @@ -92,22 +92,27 @@ const StatTitle = pure<{ isLoading: boolean; value: number | null | undefined }> ) ); -const getChartSpan = (barChart: BarChartData[] | undefined | null, areaChart: AreaChartData[] | undefined | null) => { - return barChart && barChart.length && areaChart && areaChart.length ? 5 : 10 -} +const getChartSpan = ( + barChart: BarChartData[] | undefined | null, + areaChart: AreaChartData[] | undefined | null +) => { + return barChart && barChart.length && areaChart && areaChart.length ? 5 : 10; +}; export const StatItemsComponent = pure( ({ fields, description, isLoading, key, grow, barChart, areaChart }) => ( - {description} + + {description} + } - titleSize="m" + titleSize="m" title={ - - { - fields.map(field => ( + + {fields.map(field => ( ( data-test-subj="stat-title" > - - - + + {field.color ? ( + + + + ) : null} + + + @@ -131,34 +138,32 @@ export const StatItemsComponent = pure( - )) - } - - }/> - { areaChart || barChart ? : null } - { isLoading && (areaChart || barChart) ? : ( - - { - barChart ? ( + ))} + + } + /> + {areaChart || barChart ? : null} + {isLoading && (areaChart || barChart) ? ( + + ) : ( + + {barChart ? ( - + - ) : null - } - { - areaChart ? ( + ) : null} + {areaChart ? ( - ) : null - } - - )} + ) : null} + + )} ) diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts index da52d5720a302..0531bb8a5f5d7 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts @@ -13,80 +13,80 @@ export const mockKpiHostsData: { KpiHosts: KpiHostsData } = { hosts: 1026, hostsHistogram: [ { - "doc_count": 5017216, - "key": 1556089200000, + doc_count: 5017216, + key: 1556089200000, }, { - "doc_count": 4590090, - "key": 1556132400000, + doc_count: 4590090, + key: 1556132400000, }, { - "doc_count": 73901, - "key": 1556175600000, + doc_count: 73901, + key: 1556175600000, }, ], authSuccess: 2, authSuccessHistogram: [ { - "doc_count": 1, - "key": 1556128800000, + doc_count: 1, + key: 1556128800000, }, { - "doc_count": 0, - "key": 1556139600000, + doc_count: 0, + key: 1556139600000, }, { - "doc_count": 0, - "key": 1556150400000, + doc_count: 0, + key: 1556150400000, }, { - "doc_count": 1, - "key": 1556161200000, + doc_count: 1, + key: 1556161200000, }, ], authFailure: 306495, authFailureHistogram: [ { - "doc_count": 220265, - "key": 1556089200000, + doc_count: 220265, + key: 1556089200000, }, { - "doc_count": 86135, - "key": 1556132400000, + doc_count: 86135, + key: 1556132400000, }, { - "doc_count": 95, - "key": 1556175600000, + doc_count: 95, + key: 1556175600000, }, ], uniqueSourceIps: 11929, uniqueSourceIpsHistogram: [ { - "doc_count": 1419836, - "key": 1556089200000, + doc_count: 1419836, + key: 1556089200000, }, { - "doc_count": 1074440, - "key": 1556132400000, + doc_count: 1074440, + key: 1556132400000, }, { - "doc_count": 9328, - "key": 1556175600000, + doc_count: 9328, + key: 1556175600000, }, ], uniqueDestinationIps: 2662, uniqueDestinationIpsHistogram: [ { - "doc_count": 1189146, - "key": 1556089200000, + doc_count: 1189146, + key: 1556089200000, }, { - "doc_count": 977334, - "key": 1556132400000, + doc_count: 977334, + key: 1556132400000, }, { - "doc_count": 8656, - "key": 1556175600000, + doc_count: 8656, + key: 1556175600000, }, ], }, 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 index 2e898d13dfdd6..41a3a38c6d31a 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -33,19 +33,47 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { return { hosts: getOr(null, 'responses.0.aggregations.hosts.value', response), - hostsHistogram: getOr(null, 'responses.0.aggregations.hosts_hostogram.hosts_over_time.buckets', response), - authSuccess: getOr(null, 'responses.1.aggregations.authentication_success.doc_count', response), - authSuccessHistogram: getOr(null, 'responses.1.aggregations.authentication_success.attempts_over_time.buckets', response), - authFailure: getOr(null, 'responses.1.aggregations.authentication_failure.doc_count', response), - authFailureHistogram: getOr(null, 'responses.1.aggregations.authentication_failure.attempts_over_time.buckets', response), + hostsHistogram: getOr( + null, + 'responses.0.aggregations.hosts_hostogram.hosts_over_time.buckets', + response + ), + authSuccess: getOr( + null, + 'responses.1.aggregations.authentication_success.doc_count', + response + ), + authSuccessHistogram: getOr( + null, + 'responses.1.aggregations.authentication_success.attempts_over_time.buckets', + response + ), + authFailure: getOr( + null, + 'responses.1.aggregations.authentication_failure.doc_count', + response + ), + authFailureHistogram: getOr( + null, + 'responses.1.aggregations.authentication_failure.attempts_over_time.buckets', + response + ), uniqueSourceIps: getOr(null, 'responses.0.aggregations.unique_source_ips.value', response), - uniqueSourceIpsHistogram: getOr(null, 'responses.0.aggregations.unique_source_ips_hostogram.ips_over_time.buckets', response), + uniqueSourceIpsHistogram: getOr( + null, + 'responses.0.aggregations.unique_source_ips_hostogram.ips_over_time.buckets', + response + ), uniqueDestinationIps: getOr( null, 'responses.0.aggregations.unique_destination_ips.value', response ), - uniqueDestinationIpsHistogram: getOr(null, 'responses.0.aggregations.unique_destination_ips_hostogram.ips_over_time.buckets', response), + uniqueDestinationIpsHistogram: getOr( + null, + 'responses.0.aggregations.unique_destination_ips_hostogram.ips_over_time.buckets', + response + ), }; } } diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts index 61222deb3596e..788dac739186e 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -41,464 +41,454 @@ export const mockRequest = { }; export const mockResponse = { - "took": 4405, - "responses": [ + took: 4405, + responses: [ { - "took": 4404, - "timed_out": false, - "_shards": { - "total": 71, - "successful": 71, - "skipped": 64, - "failed": 0 + took: 4404, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 64, + failed: 0, }, - "hits": { - "max_score": null, - "hits": [] + hits: { + max_score: null, + hits: [], }, - "aggregations": { - "unique_destination_ips_hostogram": { - "doc_count": 2175136, - "ips_over_time": { - "buckets": [ + aggregations: { + unique_destination_ips_hostogram: { + doc_count: 2175136, + ips_over_time: { + buckets: [ { - "key_as_string": "2019-04-24T07:00:00.000Z", - "key": 1556089200000, - "doc_count": 1189146 + key_as_string: '2019-04-24T07:00:00.000Z', + key: 1556089200000, + doc_count: 1189146, }, { - "key_as_string": "2019-04-24T19:00:00.000Z", - "key": 1556132400000, - "doc_count": 977334 + key_as_string: '2019-04-24T19:00:00.000Z', + key: 1556132400000, + doc_count: 977334, }, { - "key_as_string": "2019-04-25T07:00:00.000Z", - "key": 1556175600000, - "doc_count": 8656 - } + key_as_string: '2019-04-25T07:00:00.000Z', + key: 1556175600000, + doc_count: 8656, + }, ], - "interval": "12h" - } + interval: '12h', + }, }, - "unique_source_ips": { - "value": 11929 + unique_source_ips: { + value: 11929, }, - "hosts": { - "value": 1026 + hosts: { + value: 1026, }, - "hosts_hostogram": { - "doc_count": 9681207, - "hosts_over_time": { - "buckets": [ + hosts_hostogram: { + doc_count: 9681207, + hosts_over_time: { + buckets: [ { - "key_as_string": "2019-04-24T07:00:00.000Z", - "key": 1556089200000, - "doc_count": 5017216 + key_as_string: '2019-04-24T07:00:00.000Z', + key: 1556089200000, + doc_count: 5017216, }, { - "key_as_string": "2019-04-24T19:00:00.000Z", - "key": 1556132400000, - "doc_count": 4590090 + key_as_string: '2019-04-24T19:00:00.000Z', + key: 1556132400000, + doc_count: 4590090, }, { - "key_as_string": "2019-04-25T07:00:00.000Z", - "key": 1556175600000, - "doc_count": 73901 - } + key_as_string: '2019-04-25T07:00:00.000Z', + key: 1556175600000, + doc_count: 73901, + }, ], - "interval": "12h" - } + interval: '12h', + }, }, - "unique_destination_ips": { - "value": 2662 + unique_destination_ips: { + value: 2662, }, - "unique_source_ips_hostogram": { - "doc_count": 2503604, - "ips_over_time": { - "buckets": [ + unique_source_ips_hostogram: { + doc_count: 2503604, + ips_over_time: { + buckets: [ { - "key_as_string": "2019-04-24T07:00:00.000Z", - "key": 1556089200000, - "doc_count": 1419836 + key_as_string: '2019-04-24T07:00:00.000Z', + key: 1556089200000, + doc_count: 1419836, }, { - "key_as_string": "2019-04-24T19:00:00.000Z", - "key": 1556132400000, - "doc_count": 1074440 + key_as_string: '2019-04-24T19:00:00.000Z', + key: 1556132400000, + doc_count: 1074440, }, { - "key_as_string": "2019-04-25T07:00:00.000Z", - "key": 1556175600000, - "doc_count": 9328 - } + key_as_string: '2019-04-25T07:00:00.000Z', + key: 1556175600000, + doc_count: 9328, + }, ], - "interval": "12h" - } - } + interval: '12h', + }, + }, }, - "status": 200 + status: 200, }, { - "took": 1124, - "timed_out": false, - "_shards": { - "total": 71, - "successful": 71, - "skipped": 64, - "failed": 0 + took: 1124, + timed_out: false, + _shards: { + total: 71, + successful: 71, + skipped: 64, + failed: 0, }, - "hits": { - "max_score": null, - "hits": [] + hits: { + max_score: null, + hits: [], }, - "aggregations": { - "authentication_success": { - "doc_count": 2, - "attempts_over_time": { - "buckets": [ + aggregations: { + authentication_success: { + doc_count: 2, + attempts_over_time: { + buckets: [ { - "key_as_string": "2019-04-24T18:00:00.000Z", - "key": 1556128800000, - "doc_count": 1 + key_as_string: '2019-04-24T18:00:00.000Z', + key: 1556128800000, + doc_count: 1, }, { - "key_as_string": "2019-04-24T21:00:00.000Z", - "key": 1556139600000, - "doc_count": 0 + key_as_string: '2019-04-24T21:00:00.000Z', + key: 1556139600000, + doc_count: 0, }, { - "key_as_string": "2019-04-25T00:00:00.000Z", - "key": 1556150400000, - "doc_count": 0 + key_as_string: '2019-04-25T00:00:00.000Z', + key: 1556150400000, + doc_count: 0, }, { - "key_as_string": "2019-04-25T03:00:00.000Z", - "key": 1556161200000, - "doc_count": 1 - } + key_as_string: '2019-04-25T03:00:00.000Z', + key: 1556161200000, + doc_count: 1, + }, ], - "interval": "3h" - } + interval: '3h', + }, }, - "authentication_failure": { - "doc_count": 306495, - "attempts_over_time": { - "buckets": [ + authentication_failure: { + doc_count: 306495, + attempts_over_time: { + buckets: [ { - "key_as_string": "2019-04-24T07:00:00.000Z", - "key": 1556089200000, - "doc_count": 220265 + key_as_string: '2019-04-24T07:00:00.000Z', + key: 1556089200000, + doc_count: 220265, }, { - "key_as_string": "2019-04-24T19:00:00.000Z", - "key": 1556132400000, - "doc_count": 86135 + key_as_string: '2019-04-24T19:00:00.000Z', + key: 1556132400000, + doc_count: 86135, }, { - "key_as_string": "2019-04-25T07:00:00.000Z", - "key": 1556175600000, - "doc_count": 95 - } + key_as_string: '2019-04-25T07:00:00.000Z', + key: 1556175600000, + doc_count: 95, + }, ], - "interval": "12h" - } - } + interval: '12h', + }, + }, }, - "status": 200 - } - ] + status: 200, + }, + ], }; export const mockResult = { hosts: 1026, hostsHistogram: [ { - "doc_count": 5017216, - "key": 1556089200000, - "key_as_string": "2019-04-24T07:00:00.000Z", + doc_count: 5017216, + key: 1556089200000, + key_as_string: '2019-04-24T07:00:00.000Z', }, { - "doc_count": 4590090, - "key": 1556132400000, - "key_as_string": "2019-04-24T19:00:00.000Z", + doc_count: 4590090, + key: 1556132400000, + key_as_string: '2019-04-24T19:00:00.000Z', }, { - "doc_count": 73901, - "key": 1556175600000, - "key_as_string": "2019-04-25T07:00:00.000Z", + doc_count: 73901, + key: 1556175600000, + key_as_string: '2019-04-25T07:00:00.000Z', }, ], authSuccess: 2, authSuccessHistogram: [ { - "doc_count": 1, - "key": 1556128800000, - "key_as_string": "2019-04-24T18:00:00.000Z", + doc_count: 1, + key: 1556128800000, + key_as_string: '2019-04-24T18:00:00.000Z', }, { - "doc_count": 0, - "key": 1556139600000, - "key_as_string": "2019-04-24T21:00:00.000Z", + doc_count: 0, + key: 1556139600000, + key_as_string: '2019-04-24T21:00:00.000Z', }, { - "doc_count": 0, - "key": 1556150400000, - "key_as_string": "2019-04-25T00:00:00.000Z", + doc_count: 0, + key: 1556150400000, + key_as_string: '2019-04-25T00:00:00.000Z', }, { - "doc_count": 1, - "key": 1556161200000, - "key_as_string": "2019-04-25T03:00:00.000Z", + doc_count: 1, + key: 1556161200000, + key_as_string: '2019-04-25T03:00:00.000Z', }, ], authFailure: 306495, authFailureHistogram: [ { - "doc_count": 220265, - "key": 1556089200000, - "key_as_string": "2019-04-24T07:00:00.000Z", + doc_count: 220265, + key: 1556089200000, + key_as_string: '2019-04-24T07:00:00.000Z', }, { - "doc_count": 86135, - "key": 1556132400000, - "key_as_string": "2019-04-24T19:00:00.000Z", + doc_count: 86135, + key: 1556132400000, + key_as_string: '2019-04-24T19:00:00.000Z', }, { - "doc_count": 95, - "key": 1556175600000, - "key_as_string": "2019-04-25T07:00:00.000Z", + doc_count: 95, + key: 1556175600000, + key_as_string: '2019-04-25T07:00:00.000Z', }, ], uniqueSourceIps: 11929, uniqueSourceIpsHistogram: [ { - "doc_count": 1419836, - "key": 1556089200000, - "key_as_string": "2019-04-24T07:00:00.000Z", + doc_count: 1419836, + key: 1556089200000, + key_as_string: '2019-04-24T07:00:00.000Z', }, { - "doc_count": 1074440, - "key": 1556132400000, - "key_as_string": "2019-04-24T19:00:00.000Z", + doc_count: 1074440, + key: 1556132400000, + key_as_string: '2019-04-24T19:00:00.000Z', }, { - "doc_count": 9328, - "key": 1556175600000, - "key_as_string": "2019-04-25T07:00:00.000Z", + doc_count: 9328, + key: 1556175600000, + key_as_string: '2019-04-25T07:00:00.000Z', }, ], uniqueDestinationIps: 2662, uniqueDestinationIpsHistogram: [ { - "doc_count": 1189146, - "key": 1556089200000, - "key_as_string": "2019-04-24T07:00:00.000Z", + doc_count: 1189146, + key: 1556089200000, + key_as_string: '2019-04-24T07:00:00.000Z', }, { - "doc_count": 977334, - "key": 1556132400000, - "key_as_string": "2019-04-24T19:00:00.000Z", + doc_count: 977334, + key: 1556132400000, + key_as_string: '2019-04-24T19:00:00.000Z', }, { - "doc_count": 8656, - "key": 1556175600000, - "key_as_string": "2019-04-25T07:00:00.000Z", + doc_count: 8656, + key: 1556175600000, + key_as_string: '2019-04-25T07:00:00.000Z', }, ], }; export const mockGeneralQuery = [ { - "index": [ - "filebeat-*", - "auditbeat-*", - "packetbeat-*", - "winlogbeat-*" - ], - "allowNoIndices": true, - "ignoreUnavailable": true + index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + allowNoIndices: true, + ignoreUnavailable: true, }, { - "aggregations": { - "hosts": { - "cardinality": { - "field": "host.name" - } + aggregations: { + hosts: { + cardinality: { + field: 'host.name', + }, }, - "hosts_hostogram": { - "filter": { - "bool": { - "should": [ + hosts_hostogram: { + filter: { + bool: { + should: [ { - "exists": { - "field": "host.name" - } - } + exists: { + field: 'host.name', + }, + }, ], - "minimum_should_match": 1 - } + minimum_should_match: 1, + }, + }, + aggregations: { + hosts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + }, }, - "aggregations": { - "hosts_over_time": { - "auto_date_histogram": { - "field": "@timestamp", - "buckets": "6" - } - } - } }, - "unique_source_ips": { - "cardinality": { - "field": "source.ip" - } + unique_source_ips: { + cardinality: { + field: 'source.ip', + }, }, - "unique_source_ips_hostogram": { - "filter": { - "bool": { - "should": [ + unique_source_ips_hostogram: { + filter: { + bool: { + should: [ { - "exists": { - "field": "source.ip" - } - } + exists: { + field: 'source.ip', + }, + }, ], - "minimum_should_match": 1 - } + minimum_should_match: 1, + }, + }, + aggregations: { + ips_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6, + }, + }, }, - "aggregations": { - "ips_over_time": { - "auto_date_histogram": { - "field": "@timestamp", - "buckets": 6 - } - } - } }, - "unique_destination_ips": { - "cardinality": { - "field": "destination.ip" - } + unique_destination_ips: { + cardinality: { + field: 'destination.ip', + }, }, - "unique_destination_ips_hostogram": { - "filter": { - "bool": { - "should": [ + unique_destination_ips_hostogram: { + filter: { + bool: { + should: [ { - "exists": { - "field": "destination.ip" - } - } + exists: { + field: 'destination.ip', + }, + }, ], - "minimum_should_match": 1 - } + minimum_should_match: 1, + }, + }, + aggregations: { + ips_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6, + }, + }, }, - "aggregations": { - "ips_over_time": { - "auto_date_histogram": { - "field": "@timestamp", - "buckets": 6 - } - } - } - } + }, }, - "query": { - "bool": { - "filter": [ + query: { + bool: { + filter: [ { - "range": { - "@timestamp": { - "gte": 1556091284295, - "lte": 1556177684295 - } - } - } - ] - } + range: { + '@timestamp': { + gte: 1556091284295, + lte: 1556177684295, + }, + }, + }, + ], + }, }, - "size": 0, - "track_total_hits": false - } + size: 0, + track_total_hits: false, + }, ]; export const mockAuthQuery = [ { - "index": [ - "filebeat-*", - "auditbeat-*", - "packetbeat-*", - "winlogbeat-*" - ], - "allowNoIndices": true, - "ignoreUnavailable": true + index: ['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + allowNoIndices: true, + ignoreUnavailable: true, }, { - "aggs": { - "authentication_success": { - "filter": { - "term": { - "event.type": "authentication_success" - } + aggs: { + authentication_success: { + filter: { + term: { + 'event.type': 'authentication_success', + }, + }, + aggs: { + attempts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6, + }, + }, }, - "aggs": { - "attempts_over_time": { - "auto_date_histogram": { - "field": "@timestamp", - "buckets": 6 - } - } - } }, - "authentication_failure": { - "filter": { - "term": { - "event.type": "authentication_failure" - } + authentication_failure: { + filter: { + term: { + 'event.type': 'authentication_failure', + }, + }, + aggs: { + attempts_over_time: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6, + }, + }, }, - "aggs": { - "attempts_over_time": { - "auto_date_histogram": { - "field": "@timestamp", - "buckets": 6 - } - } - } - } + }, }, - "query": { - "bool": { - "filter": [ + query: { + bool: { + filter: [ { - "bool": { - "should": [ + bool: { + should: [ { - "match": { - "event.type": "authentication_success" - } + match: { + 'event.type': 'authentication_success', + }, }, { - "match": { - "event.type": "authentication_failure" - } - } + match: { + 'event.type': 'authentication_failure', + }, + }, ], - "minimum_should_match": 1 - } + minimum_should_match: 1, + }, }, { - "range": { - "@timestamp": { - "gte": 1556091284295, - "lte": 1556177684295 - } - } - } - ] - } + range: { + '@timestamp': { + gte: 1556091284295, + lte: 1556177684295, + }, + }, + }, + ], + }, }, - "size": 0, - "track_total_hits": false - } + size: 0, + track_total_hits: false, + }, ]; export const mockMsearchOptions = { 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 index 7cea50db5c814..55a340098d967 100644 --- 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 @@ -50,19 +50,19 @@ export const buildGeneralQuery = ({ should: [ { exists: { - field: 'host.name' - } - } + field: 'host.name', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }, aggregations: { hosts_over_time: { auto_date_histogram: { field: '@timestamp', - buckets: '6' - }, + buckets: '6', + }, }, }, }, @@ -77,19 +77,19 @@ export const buildGeneralQuery = ({ should: [ { exists: { - field: 'source.ip' - } - } + field: 'source.ip', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }, aggregations: { ips_over_time: { auto_date_histogram: { field: '@timestamp', - buckets: 6 - }, + buckets: 6, + }, }, }, }, @@ -104,19 +104,19 @@ export const buildGeneralQuery = ({ should: [ { exists: { - field: 'destination.ip' - } - } + field: 'destination.ip', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }, aggregations: { ips_over_time: { auto_date_histogram: { field: '@timestamp', - buckets: 6 - }, + buckets: 6, + }, }, }, }, diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 1d9dc502cd48a..fd01b299eb5f0 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -34,29 +34,30 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }, }) .then(resp => { - const kpiHosts = resp.data.source.KpiHosts; expect(kpiHosts!.hosts).to.equal(1); - expect(kpiHosts!.hostsHistogram).to.eql([{ - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + expect(kpiHosts!.hostsHistogram).to.eql([ + { + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - }]); + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, + ]); expect(kpiHosts!.authSuccess).to.be(0); expect(kpiHosts!.authSuccessHistogram).to.eql([]); expect(kpiHosts!.authFailure).to.equal(0); @@ -64,48 +65,48 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueSourceIpsHistogram).to.eql([ { - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - } + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, ]); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); expect(kpiHosts!.uniqueDestinationIpsHistogram).to.eql([ { - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - } + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, ]); }); }); @@ -134,26 +135,28 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { .then(resp => { const kpiHosts = resp.data.source.KpiHosts; expect(kpiHosts!.hosts).to.equal(1); - expect(kpiHosts!.hostsHistogram).to.eql([{ - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + expect(kpiHosts!.hostsHistogram).to.eql([ + { + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - }]); + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, + ]); expect(kpiHosts!.authSuccess).to.be(0); expect(kpiHosts!.authSuccessHistogram).to.eql([]); expect(kpiHosts!.authFailure).to.equal(0); @@ -161,48 +164,48 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueSourceIpsHistogram).to.eql([ { - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - } + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, ]); expect(kpiHosts!.uniqueDestinationIps).to.equal(154); expect(kpiHosts!.uniqueDestinationIpsHistogram).to.eql([ { - "x": 1549728000000, - "y": 1574, - "__typename": "HistogramData" + x: 1549728000000, + y: 1574, + __typename: 'HistogramData', }, { - "x": 1549738800000, - "y": 0, - "__typename": "HistogramData" + x: 1549738800000, + y: 0, + __typename: 'HistogramData', }, { - "x": 1549749600000, - "y": 1302, - "__typename": "HistogramData" + x: 1549749600000, + y: 1302, + __typename: 'HistogramData', }, { - "x": 1549760400000, - "y": 3281, - "__typename": "HistogramData" - } + x: 1549760400000, + y: 3281, + __typename: 'HistogramData', + }, ]); }); }); From ef5cdfd28a111ce6538bb99d650e2dfeb451bfa9 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 30 Apr 2019 15:59:21 +0800 Subject: [PATCH 08/26] rename variables --- .../siem/public/components/page/hosts/kpi_hosts/index.tsx | 4 ++-- .../public/components/page/network/kpi_network/index.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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 index cc9d864e3a161..46262d5a2e264 100644 --- 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 @@ -154,7 +154,7 @@ export const KpiHostsComponent = pure(({ data, loading }) => { if (card.areaChart != null) statItemProps = { ...statItemProps, - areaChart: addValueToChart(card.areaChart, data), + areaChart: addValueToAreaChart(card.areaChart, data), }; if (card.barChart != null) @@ -172,7 +172,7 @@ export const KpiHostsComponent = pure(({ data, loading }) => { const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); -const addValueToChart = (fields: AreaChartData[], data: KpiHostsData): AreaChartData[] => +const addValueToAreaChart = (fields: AreaChartData[], data: KpiHostsData): AreaChartData[] => fields .filter(field => get(field.key, data) != null) .map(field => ({ ...field, value: get(field.key, data) })); 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 af564a2e1d44d..4724a31842425 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 @@ -88,12 +88,12 @@ const fieldTitleMapping: Readonly = [ export const KpiNetworkComponent = pure(({ data, loading }) => { return ( - {fieldTitleMapping.map(card => ( + {fieldTitleMapping.map(stat => ( ))} From 6c808f3c600a43f7b0e23b6990bf4f08e4c90016 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Thu, 2 May 2019 15:28:39 -0400 Subject: [PATCH 09/26] simplify flex groups, add icon var, rm loading --- .../public/components/stat_items/index.tsx | 132 +++++++----------- 1 file changed, 50 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index e2fedc8151531..c3451f8456922 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -7,22 +7,17 @@ import { EuiFlexGroup, EuiFlexItem, - EuiLoadingSpinner, EuiPanel, EuiHorizontalRule, // @ts-ignore EuiStat, EuiIcon, + EuiTitle, } from '@elastic/eui'; - -const iconType = 'stopFilled'; -import numeral from '@elastic/numeral'; import React from 'react'; import { pure } from 'recompose'; - -import { EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { getEmptyTagValue } from '../empty_value'; + import { LoadingPanel } from '../loading'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; @@ -30,14 +25,10 @@ import { AreaChart } from './areachart'; export const WrappedByAutoSizer = styled.div` height: 100px; position: relative; -`; -export const ChartOverlay = styled.div` - height: 100%; - width: 100%; - top: 0; - left: 0; - position: absolute; + &:hover { + z-index: 100; + } `; export interface StatItem { @@ -45,6 +36,7 @@ export interface StatItem { description?: string; value: number | undefined | null; color?: string; + icon: string; } export interface AreaChartData { @@ -78,71 +70,36 @@ export interface StatItemsProps extends StatItems { key: string; } -const StatTitle = pure<{ isLoading: boolean; value: number | null | undefined }>( - ({ isLoading, value }) => ( - - {isLoading ? ( - - ) : value != null ? ( - numeral(value).format('0,0') - ) : ( - getEmptyTagValue() - )} - - ) -); - -const getChartSpan = ( - barChart: BarChartData[] | undefined | null, - areaChart: AreaChartData[] | undefined | null -) => { - return barChart && barChart.length && areaChart && areaChart.length ? 5 : 10; -}; - export const StatItemsComponent = pure( ({ fields, description, isLoading, key, grow, barChart, areaChart }) => ( - + - - {description} - - } - titleSize="m" - title={ - - {fields.map(field => ( - - - - - {field.color ? ( - - - - ) : null} - - - - - - - {field.description} - - - - ))} - - } - /> - {areaChart || barChart ? : null} + +
{description}
+
+ + + {fields.map(field => ( + + + + + + + + +

+ {field.value && field.value.toLocaleString()} {field.description} +

+
+
+
+
+ ))} +
+ + {areaChart || barChart ? : null} + {isLoading && (areaChart || barChart) ? ( ( data-test-subj="InitialLoadingKpisHostsChart" /> ) : ( - + {barChart ? ( - + - +
) : null} + {areaChart ? ( - + - + ) : null} )}
- + ) ); + +const FlexItem = styled(EuiFlexItem)` + min-width: 0; +`; + +const StatValue = styled(EuiTitle)` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; From 602220a436a19a6db4e5b0d5068bd1c27a89114c Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Thu, 2 May 2019 15:29:03 -0400 Subject: [PATCH 10/26] add icon names, change flex grows --- .../public/components/page/hosts/kpi_hosts/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 index 46262d5a2e264..1f368e3f08276 100644 --- 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 @@ -37,6 +37,7 @@ const fieldTitleMapping: StatItems[] = [ key: 'hosts', value: null, color: euiColorVis1, + icon: 'storage', }, ], areaChart: [ @@ -56,12 +57,14 @@ const fieldTitleMapping: StatItems[] = [ description: i18n.AUTHENTICATION_SUCCESS, value: null, color: euiColorVis0, + icon: 'check', }, { key: 'authFailure', description: i18n.AUTHENTICATION_FAILURE, value: null, color: euiColorVis9, + icon: 'cross', }, ], areaChart: [ @@ -88,7 +91,7 @@ const fieldTitleMapping: StatItems[] = [ color: euiColorVis9, }, ], - grow: 4, + grow: 5, description: i18n.AUTHENTICATION, }, { @@ -98,12 +101,14 @@ const fieldTitleMapping: StatItems[] = [ description: i18n.UNIQUE_SOURCE_IPS, value: null, color: euiColorVis2, + icon: 'visMapCoordinate', }, { key: 'uniqueDestinationIps', description: i18n.UNIQUE_DESTINATION_IPS, value: null, color: euiColorVis3, + icon: 'visMapCoordinate', }, ], areaChart: [ @@ -130,7 +135,7 @@ const fieldTitleMapping: StatItems[] = [ color: euiColorVis3, }, ], - grow: 4, + grow: 5, description: i18n.UNIQUE_PRIVATE_IPS, }, ]; From 843760f7ed56b96faed2c3af2a2439c4a1a343df Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Thu, 2 May 2019 15:30:11 -0400 Subject: [PATCH 11/26] rm overlay, enable axes, show first/last ticks --- .../components/stat_items/areachart.tsx | 14 +++++++---- .../public/components/stat_items/barchart.tsx | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 1e4a5ac5b442a..8d2e52370a980 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -6,8 +6,9 @@ import React from 'react'; import { pure } from 'recompose'; +import styled from 'styled-components'; import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; -import { AreaChartData, WrappedByAutoSizer, ChartOverlay } from '.'; +import { AreaChartData, WrappedByAutoSizer } from '.'; import { AutoSizer } from '../auto_sizer'; const ChartBaseComponent = pure<{ @@ -16,7 +17,7 @@ const ChartBaseComponent = pure<{ height: number | undefined; }>(({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( - + {data.map(series => series.value != null ? ( /** @@ -32,7 +33,7 @@ const ChartBaseComponent = pure<{ /> ) : null )} - + ) : null ); @@ -41,8 +42,13 @@ export const AreaChart = pure<{ areaChart: AreaChartData[] }>(({ 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.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 47a4eb6845bc0..941a388beee42 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -10,9 +10,11 @@ import { EuiSeriesChartUtils, } from '@elastic/eui'; import { pure } from 'recompose'; +import styled from 'styled-components'; import { EuiSeriesChart, EuiBarSeries } from '@elastic/eui/lib/experimental'; -import { BarChartData, WrappedByAutoSizer, ChartOverlay } from '.'; +import { BarChartData, WrappedByAutoSizer } from '.'; import { AutoSizer } from '../auto_sizer'; + const { SCALE, ORIENTATION } = EuiSeriesChartUtils; const ChartBaseComponent = pure<{ @@ -21,12 +23,7 @@ const ChartBaseComponent = pure<{ height: number | undefined; }>(({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( - + {data.map(series => series.value != null ? ( /** @@ -41,7 +38,7 @@ const ChartBaseComponent = pure<{ /> ) : null )} - + ) : null ); @@ -50,8 +47,16 @@ export const BarChart = pure<{ barChart: BarChartData[] }>(({ 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; + } +`; From 7233f658c4f0c84c1bc1e1e6892015820a898091 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Thu, 2 May 2019 15:30:26 -0400 Subject: [PATCH 12/26] unabbreviate destination --- .../siem/public/components/page/hosts/kpi_hosts/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e6e4e5933eea4..46d8a5b879464 100644 --- 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 @@ -49,6 +49,6 @@ export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniq export const UNIQUE_DESTINATION_IPS = i18n.translate( 'xpack.siem.kpiHosts.source.uniqueDestinationIpsTitle', { - defaultMessage: 'Dest.', + defaultMessage: 'Destination', } ); From 12371affb2b44f8bebb606bf4dc8e15ca0b35700 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Fri, 3 May 2019 17:32:42 +0800 Subject: [PATCH 13/26] update UI --- .../components/page/hosts/kpi_hosts/index.tsx | 104 +-- .../page/hosts/kpi_hosts/translations.ts | 9 +- .../page/network/kpi_network/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 713 +++++++++++++++--- .../components/stat_items/areachart.tsx | 4 +- .../public/components/stat_items/barchart.tsx | 4 +- .../components/stat_items/index.test.tsx | 140 ++-- .../public/components/stat_items/index.tsx | 107 +-- 8 files changed, 782 insertions(+), 301 deletions(-) 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 index 1f368e3f08276..32a55fa40d90f 100644 --- 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 @@ -8,6 +8,8 @@ import { EuiFlexGroup } from '@elastic/eui'; import { get } 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, @@ -40,13 +42,7 @@ const fieldTitleMapping: StatItems[] = [ icon: 'storage', }, ], - areaChart: [ - { - key: 'hostsHistogram', - value: null, - color: euiColorVis1, - }, - ], + enableAreaChart: true, grow: 2, description: i18n.HOSTS, }, @@ -67,31 +63,9 @@ const fieldTitleMapping: StatItems[] = [ icon: 'cross', }, ], - areaChart: [ - { - key: 'authSuccessHistogram', - value: null, - color: euiColorVis0, - }, - { - key: 'authFailureHistogram', - value: null, - color: euiColorVis9, - }, - ], - barChart: [ - { - key: 'authSuccess', - value: null, - color: euiColorVis0, - }, - { - key: 'authFailure', - value: null, - color: euiColorVis9, - }, - ], - grow: 5, + enableAreaChart: true, + enableBarChart: true, + grow: 4, description: i18n.AUTHENTICATION, }, { @@ -111,61 +85,45 @@ const fieldTitleMapping: StatItems[] = [ icon: 'visMapCoordinate', }, ], - areaChart: [ - { - key: 'uniqueSourceIpsHistogram', - value: null, - color: euiColorVis2, - }, - { - key: 'uniqueDestinationIpsHistogram', - value: null, - color: euiColorVis3, - }, - ], - barChart: [ - { - key: 'uniqueSourceIps', - value: null, - color: euiColorVis2, - }, - { - key: 'uniqueDestinationIps', - value: null, - color: euiColorVis3, - }, - ], - grow: 5, + enableAreaChart: true, + enableBarChart: true, + grow: 4, description: i18n.UNIQUE_PRIVATE_IPS, }, ]; export const KpiHostsComponent = pure(({ data, loading }) => { - return ( + return loading ? ( + + + + + + ) : ( - {fieldTitleMapping.map(card => { + {fieldTitleMapping.map(stat => { let statItemProps: StatItemsProps = { - ...card, + ...stat, isLoading: loading, - key: `kpi-hosts-summary-${card.description}`, + key: `kpi-hosts-summary-${stat.description}`, }; - if (card.fields != null) + if (stat.fields != null) statItemProps = { ...statItemProps, - fields: addValueToFields(card.fields, data), + fields: addValueToFields(stat.fields, data), }; - if (card.areaChart != null) + if (stat.enableAreaChart) statItemProps = { ...statItemProps, - areaChart: addValueToAreaChart(card.areaChart, data), + areaChart: addValueToAreaChart(stat.fields, data), }; - if (card.barChart != null) + if (stat.enableBarChart != null) statItemProps = { ...statItemProps, - barChart: addValueToBarChart(card.barChart, data), + barChart: addValueToBarChart(stat.fields, data), }; return ; @@ -177,12 +135,16 @@ export const KpiHostsComponent = pure(({ data, loading }) => { const addValueToFields = (fields: StatItem[], data: KpiHostsData): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); -const addValueToAreaChart = (fields: AreaChartData[], data: KpiHostsData): AreaChartData[] => +const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartData[] => fields - .filter(field => get(field.key, data) != null) - .map(field => ({ ...field, value: get(field.key, data) })); + .filter(field => get(`${field.key}Histogram`, data) != null) + .map(field => ({ + ...field, + value: get(`${field.key}Histogram`, data), + key: `${field.key}Histogram`, + })); -const addValueToBarChart = (fields: BarChartData[], data: KpiHostsData): BarChartData[] => { +const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { return fields .filter(field => get(field.key, data) != null) .map(field => { 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 index 46d8a5b879464..cf343f8dfca41 100644 --- 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 @@ -35,12 +35,9 @@ export const ACTIVE_USERS = i18n.translate('xpack.siem.kpiHosts.source.activeUse defaultMessage: 'Active Users', }); -export const UNIQUE_PRIVATE_IPS = i18n.translate( - 'xpack.siem.kpiHosts.source.uniquePrivateIpsTitle', - { - defaultMessage: 'Unique Private IPs', - } -); +export const UNIQUE_PRIVATE_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', 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 4724a31842425..306c6628e1c4d 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 @@ -90,7 +90,7 @@ export const KpiNetworkComponent = pure(({ data, loading }) => {fieldTitleMapping.map(stat => ( - +
@@ -142,6 +212,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] title={ @@ -177,7 +248,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] > @@ -186,7 +257,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] component="span" grow={4} > - UNIQUE_SOURCE_PRIVATE_IPS + Source @@ -213,7 +284,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] component="span" > @@ -223,7 +294,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] > @@ -232,7 +303,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] component="span" grow={4} > - UNIQUE_DESTINATION_PRIVATE_IPS + Dest. @@ -273,14 +344,15 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] > @@ -328,7 +400,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] height="16" style={ Object { - "fill": "#000000", + "fill": "#DB1374", } } viewBox="0 0 16 16" @@ -341,7 +413,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] height="16" style={ Object { - "fill": "#000000", + "fill": "#DB1374", } } viewBox="0 0 16 16" @@ -369,14 +441,14 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] > - -- + 1,714 @@ -393,7 +465,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] - UNIQUE_SOURCE_PRIVATE_IPS + Source @@ -402,7 +474,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] @@ -450,7 +522,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] height="16" style={ Object { - "fill": "#000000", + "fill": "#490092", } } viewBox="0 0 16 16" @@ -463,7 +535,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] height="16" style={ Object { - "fill": "#000000", + "fill": "#490092", } } viewBox="0 0 16 16" @@ -491,14 +563,14 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] > - -- + 2,359 @@ -515,7 +587,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] - UNIQUE_DESTINATION_PRIVATE_IPS + Dest. @@ -549,14 +621,24 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] barChart={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIps", + "value": Array [ + Object { + "x": 1714, + "y": "uniqueSourceIps", + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIps", + "value": Array [ + Object { + "x": 2354, + "y": "uniqueDestinationIps", + }, + ], }, ] } @@ -565,14 +647,24 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] barChart={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIps", + "value": Array [ + Object { + "x": 1714, + "y": "uniqueSourceIps", + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIps", + "value": Array [ + Object { + "x": 2354, + "y": "uniqueDestinationIps", + }, + ], }, ] } @@ -593,14 +685,24 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] data={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIps", + "value": Array [ + Object { + "x": 1714, + "y": "uniqueSourceIps", + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIps", + "value": Array [ + Object { + "x": 2354, + "y": "uniqueDestinationIps", + }, + ], }, ] } @@ -609,14 +711,24 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] data={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIps", + "value": Array [ + Object { + "x": 1714, + "y": "uniqueSourceIps", + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIps", + "value": Array [ + Object { + "x": 2354, + "y": "uniqueDestinationIps", + }, + ], }, ] } @@ -644,14 +756,40 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] areaChart={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 580213, + }, + Object { + "x": 1556730000000, + "y": 1096175, + }, + Object { + "x": 1556773200000, + "y": 12382, + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 565975, + }, + Object { + "x": 1556730000000, + "y": 1084366, + }, + Object { + "x": 1556773200000, + "y": 12280, + }, + ], }, ] } @@ -660,14 +798,40 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] areaChart={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 580213, + }, + Object { + "x": 1556730000000, + "y": 1096175, + }, + Object { + "x": 1556773200000, + "y": 12382, + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 565975, + }, + Object { + "x": 1556730000000, + "y": 1084366, + }, + Object { + "x": 1556773200000, + "y": 12280, + }, + ], }, ] } @@ -688,14 +852,40 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] data={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 580213, + }, + Object { + "x": 1556730000000, + "y": 1096175, + }, + Object { + "x": 1556773200000, + "y": 12382, + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 565975, + }, + Object { + "x": 1556730000000, + "y": 1084366, + }, + Object { + "x": 1556773200000, + "y": 12280, + }, + ], }, ] } @@ -704,14 +894,40 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] data={ Array [ Object { - "color": "#000000", - "key": "uniqueSourcePrivateIps", - "value": null, + "color": "#DB1374", + "key": "uniqueSourceIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 580213, + }, + Object { + "x": 1556730000000, + "y": 1096175, + }, + Object { + "x": 1556773200000, + "y": 12382, + }, + ], }, Object { - "color": "#000000", - "key": "uniqueDestinationPrivateIps", - "value": null, + "color": "#490092", + "key": "uniqueDestinationIpsHistogram", + "value": Array [ + Object { + "x": 1556686800000, + "y": 565975, + }, + Object { + "x": 1556730000000, + "y": 1084366, + }, + Object { + "x": 1556773200000, + "y": 12280, + }, + ], }, ] } @@ -741,11 +957,12 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] exports[`Stat Items rendering kpis without charts it renders the default widget 1`] = ` - +
@@ -784,7 +1000,7 @@ exports[`Stat Items rendering kpis without charts it renders the default widget size="xxs" > - UNIQUE_PRIVATE_IPS + HOSTS } @@ -793,6 +1009,7 @@ exports[`Stat Items rendering kpis without charts it renders the default widget title={ - UNIQUE_PRIVATE_IPS + HOSTS

@@ -867,14 +1084,15 @@ exports[`Stat Items rendering kpis without charts it renders the default widget > `; + +exports[`Stat Items rendering kpis without charts it renders the default widget 2`] = ` + + + +
+ +
+ + + HOSTS + + + } + reverse={false} + textAlign="left" + title={ + + + + + + + + + + + + + + + } + titleColor="default" + titleSize="m" + > +
+ +
+

+ + + HOSTS + + +

+
+
+ +

+ + + + + + + + + + + + + + + + -- + + + + + + + + + + + + + + + + + + +

+
+
+
+ +
+ +
+ + +
+ +
+ +
+ +
+ + + +`; diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 8d2e52370a980..61b95b757d439 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -16,7 +16,9 @@ const ChartBaseComponent = pure<{ width: number | undefined; height: number | undefined; }>(({ data, ...chartConfigs }) => - chartConfigs.width && chartConfigs.height ? ( + chartConfigs.width && + chartConfigs.height && + data.every(({ value }) => value != null && value.length > 0) ? ( {data.map(series => series.value != null ? ( diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 941a388beee42..8208f540526d1 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -22,7 +22,9 @@ const ChartBaseComponent = pure<{ width: number | undefined; height: number | undefined; }>(({ data, ...chartConfigs }) => - chartConfigs.width && chartConfigs.height ? ( + chartConfigs.width && + chartConfigs.height && + data.every(({ value }) => value != null && value.length > 0) ? ( {data.map(series => series.value != null ? ( 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 index d5fe3a7b1e45f..1343f39865521 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -34,22 +34,30 @@ describe('Stat Items', () => { }); }); - describe('rendering kpis without charts', () => { - const mockStatItemsData: StatItemsProps = { - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: null, - }, - ], - description: 'UNIQUE_PRIVATE_IPS', - isLoading: false, - key: 'mock-keys', - }; - let wrapper: ReactWrapper; - beforeAll(() => { - wrapper = mount(); - }); + describe.each([ + [ + mount( + + ), + ], + [ + mount( + + ), + ], + ])('rendering kpis without charts', wrapper => { test('it renders the default widget', () => { expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -62,15 +70,15 @@ describe('Stat Items', () => { expect(wrapper.find(EuiIcon)).toHaveLength(0); }); - test('should render barChart', () => { + test('should not render barChart', () => { expect(wrapper.find(BarChart)).toHaveLength(0); }); - test('should render areaChart', () => { + test('should not render areaChart', () => { expect(wrapper.find(AreaChart)).toHaveLength(0); }); - test('should render spliter', () => { + test('should not render spliter', () => { expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0); }); }); @@ -78,41 +86,35 @@ describe('Stat Items', () => { describe('rendering kpis with charts', () => { const mockStatItemsData: StatItemsProps = { fields: [ - { - key: 'uniqueSourcePrivateIps', - description: 'UNIQUE_SOURCE_PRIVATE_IPS', - value: null, - color: '#000000', - }, - { - key: 'uniqueDestinationPrivateIps', - description: 'UNIQUE_DESTINATION_PRIVATE_IPS', - value: null, - color: '#000000', - }, + { key: 'uniqueSourceIps', description: 'Source', value: 1714, color: '#DB1374' }, + { key: 'uniqueDestinationIps', description: 'Dest.', value: 2359, color: '#490092' }, ], areaChart: [ { - key: 'uniqueSourcePrivateIps', - value: null, - color: '#000000', + key: 'uniqueSourceIpsHistogram', + value: [ + { x: 1556686800000, y: 580213 }, + { x: 1556730000000, y: 1096175 }, + { x: 1556773200000, y: 12382 }, + ], + color: '#DB1374', }, { - key: 'uniqueDestinationPrivateIps', - value: null, - color: '#000000', + 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: 'uniqueSourcePrivateIps', - value: null, - color: '#000000', - }, - { - key: 'uniqueDestinationPrivateIps', - value: null, - color: '#000000', + key: 'uniqueDestinationIps', + value: [{ x: 2354, y: 'uniqueDestinationIps' }], + color: '#490092', }, ], description: 'UNIQUE_PRIVATE_IPS', @@ -147,4 +149,52 @@ describe('Stat Items', () => { expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); }); }); + + describe('areaChart data not available', () => { + const mockStatItemsData: StatItemsProps = { + fields: [ + { key: 'uniqueSourceIps', description: 'Source', value: 1714, color: '#DB1374' }, + { key: 'uniqueDestinationIps', description: 'Dest.', value: 2359, color: '#490092' }, + ], + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: [], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [], + 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', + isLoading: false, + key: 'mock-keys', + }; + let wrapper: ReactWrapper; + beforeAll(() => { + wrapper = mount(); + }); + + test('should render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(1); + }); + + test('should not render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(0); + }); + + test('should render spliter', () => { + 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 index c3451f8456922..f2276f67ba7e5 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; -import { LoadingPanel } from '../loading'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; @@ -36,13 +35,13 @@ export interface StatItem { description?: string; value: number | undefined | null; color?: string; - icon: string; + icon?: 'storage' | 'cross' | 'check' | 'visMapCoordinate'; } export interface AreaChartData { key: string; - value: ChartData[] | null; - color: string; + value: ChartData[] | [] | null; + color?: string | undefined; } export interface ChartData { @@ -53,78 +52,84 @@ export interface ChartData { export interface BarChartData { key: string; - value: ChartData[] | null; - color: string; + value: ChartData[] | [] | null; + color?: string | undefined; } export interface StatItems { fields: StatItem[]; description?: string; - areaChart?: AreaChartData[]; - barChart?: BarChartData[]; + enableAreaChart?: boolean; + enableBarChart?: boolean; grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; } export interface StatItemsProps extends StatItems { isLoading: boolean; key: string; + areaChart?: AreaChartData[]; + barChart?: BarChartData[]; } export const StatItemsComponent = pure( - ({ fields, description, isLoading, key, grow, barChart, areaChart }) => ( - - - -
{description}
-
- - - {fields.map(field => ( - - - - - - - - -

- {field.value && field.value.toLocaleString()} {field.description} -

-
-
-
-
- ))} -
- - {areaChart || barChart ? : null} - - {isLoading && (areaChart || barChart) ? ( - - ) : ( + ({ fields, description, isLoading, key, grow, barChart, areaChart }) => { + 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); + const chartExist = isBarChartDataAbailable || isAreaChartDataAvailable; + return ( + + + +
{description}
+
+ + + {fields.map(field => ( + + + {(isBarChartDataAbailable || isBarChartDataAbailable) && field.icon ? ( + + + + ) : null} + + + +

+ {field.value && field.value.toLocaleString()} {field.description} +

+
+
+
+
+ ))} +
+ + {chartExist ? : null} + {barChart ? ( - + {isBarChartDataAbailable ? : null} ) : null} {areaChart ? ( - + {isAreaChartDataAvailable ? : null} ) : null} - )} -
-
- ) +
+
+ ); + } ); const FlexItem = styled(EuiFlexItem)` From f4f2054a80fbd81fd656649954e3e7e6b74a1fce Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sat, 4 May 2019 17:28:13 +0800 Subject: [PATCH 14/26] update histogram data --- .../server/graphql/kpi_hosts/schema.gql.ts | 6 ++ x-pack/plugins/siem/server/graphql/types.ts | 34 +++++++++ .../lib/kpi_hosts/elasticsearch_adapter.ts | 15 ++-- .../plugins/siem/server/lib/kpi_hosts/mock.ts | 12 +-- .../lib/kpi_hosts/query_authentication.dsl.ts | 30 ++++++-- .../server/lib/kpi_hosts/query_general.dsl.ts | 75 ++++++------------- 6 files changed, 97 insertions(+), 75 deletions(-) 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 index de291c8f812c4..53374b0141b8b 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts @@ -7,10 +7,16 @@ import gql from 'graphql-tag'; export const kpiHostsSchema = gql` + type Count { + value: Float + doc_count: Float + } + type HistogramData { key: Float doc_count: Float key_as_string: String + count: Count } type KpiHostsData { diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index 51eeda7204dc2..3668889f79735 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -1101,6 +1101,14 @@ export interface HistogramData { doc_count?: number | null; key_as_string?: string | null; + + count?: Count | null; +} + +export interface Count { + value?: number | null; + + doc_count?: number | null; } export interface NetworkTopNFlowData { @@ -5292,6 +5300,8 @@ export namespace HistogramDataResolvers { doc_count?: DocCountResolver; key_as_string?: KeyAsStringResolver; + + count?: CountResolver; } export type KeyResolver< @@ -5309,6 +5319,30 @@ export namespace HistogramDataResolvers { 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 { 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 index 41a3a38c6d31a..9c3d2e25e13b6 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -30,14 +30,9 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { body: [...generalQuery, ...authQuery], } ); - return { hosts: getOr(null, 'responses.0.aggregations.hosts.value', response), - hostsHistogram: getOr( - null, - 'responses.0.aggregations.hosts_hostogram.hosts_over_time.buckets', - response - ), + hostsHistogram: getOr(null, 'responses.0.aggregations.hosts_histogram.buckets', response), authSuccess: getOr( null, 'responses.1.aggregations.authentication_success.doc_count', @@ -45,7 +40,7 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { ), authSuccessHistogram: getOr( null, - 'responses.1.aggregations.authentication_success.attempts_over_time.buckets', + 'responses.1.aggregations.authentication_success_histogram.buckets', response ), authFailure: getOr( @@ -55,13 +50,13 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { ), authFailureHistogram: getOr( null, - 'responses.1.aggregations.authentication_failure.attempts_over_time.buckets', + '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_hostogram.ips_over_time.buckets', + 'responses.0.aggregations.unique_source_ips_histogram.buckets', response ), uniqueDestinationIps: getOr( @@ -71,7 +66,7 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { ), uniqueDestinationIpsHistogram: getOr( null, - 'responses.0.aggregations.unique_destination_ips_hostogram.ips_over_time.buckets', + 'responses.0.aggregations.unique_destination_ips_histogram.buckets', response ), }; diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts index 788dac739186e..b1e978eba3fb1 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -57,7 +57,7 @@ export const mockResponse = { hits: [], }, aggregations: { - unique_destination_ips_hostogram: { + unique_destination_ips_histogram: { doc_count: 2175136, ips_over_time: { buckets: [ @@ -86,7 +86,7 @@ export const mockResponse = { hosts: { value: 1026, }, - hosts_hostogram: { + hosts_histogram: { doc_count: 9681207, hosts_over_time: { buckets: [ @@ -112,7 +112,7 @@ export const mockResponse = { unique_destination_ips: { value: 2662, }, - unique_source_ips_hostogram: { + unique_source_ips_histogram: { doc_count: 2503604, ips_over_time: { buckets: [ @@ -320,7 +320,7 @@ export const mockGeneralQuery = [ field: 'host.name', }, }, - hosts_hostogram: { + hosts_histogram: { filter: { bool: { should: [ @@ -347,7 +347,7 @@ export const mockGeneralQuery = [ field: 'source.ip', }, }, - unique_source_ips_hostogram: { + unique_source_ips_histogram: { filter: { bool: { should: [ @@ -374,7 +374,7 @@ export const mockGeneralQuery = [ field: 'destination.ip', }, }, - unique_destination_ips_hostogram: { + unique_destination_ips_histogram: { filter: { bool: { should: [ 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 index 4b6effcf7aa58..c9056eacd8c54 100644 --- 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 @@ -66,11 +66,18 @@ export const buildAuthQuery = ({ 'event.type': 'authentication_success', }, }, + }, + authentication_success_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, aggs: { - attempts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, + count: { + filter: { + term: { + 'event.type': 'authentication_success', + }, }, }, }, @@ -81,11 +88,18 @@ export const buildAuthQuery = ({ 'event.type': 'authentication_failure', }, }, + }, + authentication_failure_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, aggs: { - attempts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, + count: { + filter: { + term: { + 'event.type': 'authentication_failure', + }, }, }, }, 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 index 55a340098d967..52c9ccfa51767 100644 --- 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 @@ -44,24 +44,15 @@ export const buildGeneralQuery = ({ field: 'host.name', }, }, - hosts_hostogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - minimum_should_match: 1, - }, + hosts_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', }, - aggregations: { - hosts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: '6', + aggs: { + count: { + cardinality: { + field: 'host.name', }, }, }, @@ -71,24 +62,15 @@ export const buildGeneralQuery = ({ field: 'source.ip', }, }, - unique_source_ips_hostogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'source.ip', - }, - }, - ], - minimum_should_match: 1, - }, + unique_source_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', }, - aggregations: { - ips_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, + aggs: { + count: { + cardinality: { + field: 'source.ip', }, }, }, @@ -98,24 +80,15 @@ export const buildGeneralQuery = ({ field: 'destination.ip', }, }, - unique_destination_ips_hostogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'destination.ip', - }, - }, - ], - minimum_should_match: 1, - }, + unique_destination_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', }, - aggregations: { - ips_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, + aggs: { + count: { + cardinality: { + field: 'destination.ip', }, }, }, From 0ace461a6576de3a13d3fd56a4c0fdfe01bd5792 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sat, 4 May 2019 17:29:07 +0800 Subject: [PATCH 15/26] customize chart axis --- .../components/page/hosts/kpi_hosts/index.tsx | 8 +- .../page/network/kpi_network/index.tsx | 10 +- .../__snapshots__/index.test.tsx.snap | 1626 ++++++----------- .../components/stat_items/areachart.tsx | 46 +- .../public/components/stat_items/barchart.tsx | 54 +- .../public/components/stat_items/index.tsx | 44 +- .../containers/kpi_hosts/index.gql_query.ts | 23 +- .../public/containers/kpi_hosts/index.tsx | 37 +- .../siem/public/graphql/introspection.json | 35 + x-pack/plugins/siem/public/graphql/types.ts | 64 +- 10 files changed, 777 insertions(+), 1170 deletions(-) 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 index 32a55fa40d90f..030e15d04736d 100644 --- 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 @@ -5,7 +5,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import { get } from 'lodash/fp'; +import { get, getOr } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -94,7 +94,7 @@ const fieldTitleMapping: StatItems[] = [ export const KpiHostsComponent = pure(({ data, loading }) => { return loading ? ( - + @@ -147,13 +147,13 @@ const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartD const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { return fields .filter(field => get(field.key, data) != null) - .map(field => { + .map((field, idx) => { return { ...field, value: [ { x: get(field.key, data), - y: field.key, + y: getOr('', `${idx}.description`, fields), }, ], }; 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 306c6628e1c4d..0cad3183c225c 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,6 +9,8 @@ import { get } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { StatItem, StatItems, StatItemsComponent } from '../../../../components/stat_items'; import { KpiNetworkData } from '../../../../graphql/types'; @@ -86,7 +88,13 @@ const fieldTitleMapping: Readonly = [ ]; export const KpiNetworkComponent = pure(({ data, loading }) => { - return ( + return loading ? ( + + + + + + ) : ( {fieldTitleMapping.map(stat => ( - -
+ - -
- + +
- - UNIQUE_PRIVATE_IPS - - - } - reverse={false} - textAlign="left" - title={ - + + +
- - - - - - - - - + + +
+ + +

+ 1,714 + + Source +

+
+
+
+
+
+
- - - Source - -
- - + + + - - - - - - - - - - - - Dest. - - - - - } - titleColor="default" - titleSize="m" - > -
- -
-

- - - UNIQUE_PRIVATE_IPS - - -

-
-
- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - 1,714 - - - - - - - - - - - - Source - - - - - - - - - - + - - - + - - - - - - - - - - - - - - - - - - 2,359 - - - - - - - - - - - - Dest. - - - - - - - - -

-
-
- - -
-
- -
- + + +
+ +
+
+ +
+
+ +
+ + +
+
+ +
-
- + +
+ - + - - -
- +
+ - + - - -
+ ] + } + /> + +
-
- - - -
-
- - -
- + + +
+
+ + + +
+ - + - - -
- + + +
+ - + - - -
+ ] + } + /> + +
-
-
-
- -
-
- -
- -
- -
- + + + +
+ + +
+
+
+ +
+
+ `; @@ -982,201 +682,91 @@ exports[`Stat Items rendering kpis without charts it renders the default widget } isLoading={false} > - -
+ - -
- + +
- - HOSTS - - - } - reverse={false} - textAlign="left" - title={ - + + +
- - - - - - - - - - - - - } - titleColor="default" - titleSize="m" - > -
- -
-

- - - HOSTS - - -

-
-
- -

- - - - - - + - - - + - - - - - - - -- - - - - - - - - - - - - - - - - - - -

-
-
- - -
- -
- -
- + +

+ + +
+
+ +
+
+
+
+ +
+ + +
+ +
+ +
+ + `; @@ -1212,216 +802,110 @@ exports[`Stat Items rendering kpis without charts it renders the default widget } isLoading={false} > - -
+ - -
- + +
- - HOSTS - - - } - reverse={false} - textAlign="left" - title={ - + + +
- - - - - - - - - - - - - } - titleColor="default" - titleSize="m" - > -
- -
-

- - - HOSTS - - -

-
-
- -

- - - - - - + - - - + - - - - - - - -- - - - - - - - - - - - - - - - - - - -

-
-
- - -
- -
- - + + +
+
+ +
+
+
+ + +
+ + +
-
- -
- -
-
-
-
+ + +
+ + + + +
+ + +
+ +
+ +
+ + `; diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 61b95b757d439..9a8d2402b8944 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -7,37 +7,46 @@ import React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; -import { EuiSeriesChart, EuiAreaSeries } from '@elastic/eui/lib/experimental'; -import { AreaChartData, WrappedByAutoSizer } from '.'; +// @ts-ignore +import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; +import { clone as _clone } from 'lodash'; +import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.'; import { AutoSizer } from '../auto_sizer'; const ChartBaseComponent = pure<{ data: AreaChartData[]; width: number | undefined; height: number | undefined; -}>(({ data, ...chartConfigs }) => - chartConfigs.width && - chartConfigs.height && - data.every(({ value }) => value != null && value.length > 0) ? ( - - {data.map(series => - series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ +}>(({ data, ...chartConfigs }) => { + return chartConfigs.width && + chartConfigs.height && + data && + data.length && + data.every(({ value }) => value != null && value.length > 0) ? ( + + {data.map(series => { + return ( // @ts-ignore - ) : null - )} + ); + })} + {/* +// @ts-ignore */} + value.toString().split('T')[0]} /> + {/* +// @ts-ignore */} + - ) : null -); + ) : ( + + ); +}); export const AreaChart = pure<{ areaChart: AreaChartData[] }>(({ areaChart }) => ( @@ -49,6 +58,7 @@ export const AreaChart = pure<{ areaChart: AreaChartData[] }>(({ areaChart }) => )); +// @ts-ignore 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.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 8208f540526d1..6548a12e0e1d4 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -11,38 +11,55 @@ import { } from '@elastic/eui'; import { pure } from 'recompose'; import styled from 'styled-components'; -import { EuiSeriesChart, EuiBarSeries } from '@elastic/eui/lib/experimental'; -import { BarChartData, WrappedByAutoSizer } from '.'; +// @ts-ignore +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(); + return label.length > 4 ? `${label.slice(0, 4)}.` : label; +}; const ChartBaseComponent = pure<{ data: BarChartData[]; width: number | undefined; height: number | undefined; -}>(({ data, ...chartConfigs }) => - chartConfigs.width && - chartConfigs.height && - data.every(({ value }) => value != null && value.length > 0) ? ( - - {data.map(series => - series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ +}>(({ data, ...chartConfigs }) => { + return chartConfigs.width && + chartConfigs.height && + data && + data.length && + data.every(({ value }) => value != null && value.length > 0) ? ( + + {data.map(series => { + return ( // @ts-ignore - ) : null - )} + ); + })} + {/* +// @ts-ignore */} + + {/* +// @ts-ignore */} + - ) : null -); + ) : ( + + ); +}); export const BarChart = pure<{ barChart: BarChartData[] }>(({ barChart }) => ( @@ -54,6 +71,7 @@ export const BarChart = pure<{ barChart: BarChartData[] }>(({ barChart }) => ( )); +// @ts-ignore const SeriesChart = styled(EuiSeriesChart)` svg .rv-xy-plot__axis--horizontal diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index f2276f67ba7e5..cf822f39d39e3 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -18,8 +18,10 @@ 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; @@ -40,7 +42,7 @@ export interface StatItem { export interface AreaChartData { key: string; - value: ChartData[] | [] | null; + value: ChartData[] | null; color?: string | undefined; } @@ -72,7 +74,17 @@ export interface StatItemsProps extends StatItems { } export const StatItemsComponent = pure( - ({ fields, description, isLoading, key, grow, barChart, areaChart }) => { + ({ + fields, + description, + isLoading, + key, + grow, + barChart, + areaChart, + enableAreaChart, + enableBarChart, + }) => { const isBarChartDataAbailable = barChart && barChart.length && @@ -81,7 +93,6 @@ export const StatItemsComponent = pure( areaChart && areaChart.length && areaChart.every(item => item.value != null && item.value.length > 0); - const chartExist = isBarChartDataAbailable || isAreaChartDataAvailable; return ( @@ -93,7 +104,7 @@ export const StatItemsComponent = pure( {fields.map(field => ( - {(isBarChartDataAbailable || isBarChartDataAbailable) && field.icon ? ( + {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon ? ( @@ -101,8 +112,9 @@ export const StatItemsComponent = pure( -

- {field.value && field.value.toLocaleString()} {field.description} +

+ {field.value ? field.value.toLocaleString() : getEmptyTagValue()}{' '} + {field.description}

@@ -111,18 +123,18 @@ export const StatItemsComponent = pure( ))}
- {chartExist ? : null} + {enableAreaChart || enableBarChart ? : null} - {barChart ? ( + {enableBarChart ? ( - {isBarChartDataAbailable ? : null} + ) : null} - {areaChart ? ( + {enableAreaChart ? ( - {isAreaChartDataAvailable ? : null} + ) : null} @@ -132,6 +144,16 @@ export const StatItemsComponent = pure( } ); +export const ChartHolder = () => ( + + + + Chart Data Not Available + + + +); + const FlexItem = styled(EuiFlexItem)` min-width: 0; `; 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 index fc631e503a093..97bcb9d7907c1 100644 --- 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 @@ -7,34 +7,37 @@ 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 { - x: key - y: doc_count + ...ChartFields } authSuccess authSuccessHistogram { - x: key - y: doc_count + ...ChartFields } authFailure authFailureHistogram { - x: key - y: doc_count + ...ChartFields } uniqueSourceIps uniqueSourceIpsHistogram { - x: key - y: doc_count + ...ChartFields } uniqueDestinationIps uniqueDestinationIpsHistogram { - x: key - y: doc_count + ...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 index 4bd639ae1c4d1..5ee3623015b91 100644 --- a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; +import { getOr, get } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; import { pure } from 'recompose'; @@ -15,6 +15,7 @@ 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; @@ -27,6 +28,18 @@ 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 }) => ( @@ -45,9 +58,29 @@ export const KpiHostsQuery = pure( > {({ 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: { + ...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 54bcf7dfde05c..c3846e31ff4d9 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -6428,6 +6428,41 @@ "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, diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 4040ccd25710f..71e64c87e7bb6 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -1072,6 +1072,14 @@ export interface HistogramData { doc_count?: number | null; key_as_string?: string | null; + + count?: Count | null; +} + +export interface Count { + value?: number | null; + + doc_count?: number | null; } export interface NetworkTopNFlowData { @@ -2441,45 +2449,15 @@ export namespace GetKpiHostsQuery { uniqueDestinationIpsHistogram?: (UniqueDestinationIpsHistogram | null)[] | null; }; - export type HostsHistogram = { - __typename?: 'HistogramData'; - - x?: number | null; - - y?: number | null; - }; - - export type AuthSuccessHistogram = { - __typename?: 'HistogramData'; - - x?: number | null; - - y?: number | null; - }; - - export type AuthFailureHistogram = { - __typename?: 'HistogramData'; - - x?: number | null; - - y?: number | null; - }; - - export type UniqueSourceIpsHistogram = { - __typename?: 'HistogramData'; + export type HostsHistogram = ChartFields.Fragment; - x?: number | null; + export type AuthSuccessHistogram = ChartFields.Fragment; - y?: number | null; - }; + export type AuthFailureHistogram = ChartFields.Fragment; - export type UniqueDestinationIpsHistogram = { - __typename?: 'HistogramData'; - - x?: number | null; + export type UniqueSourceIpsHistogram = ChartFields.Fragment; - y?: number | null; - }; + export type UniqueDestinationIpsHistogram = ChartFields.Fragment; } export namespace GetKpiNetworkQuery { @@ -3864,3 +3842,19 @@ 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; + }; +} From 45c9e575c1790053fd5c254e8970dc870d0a663f Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 5 May 2019 14:56:23 +0800 Subject: [PATCH 16/26] update x-axis for area chart --- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/page/hosts/kpi_hosts/index.tsx | 1 - .../__snapshots__/index.test.tsx.snap | 655 ++++++++++-------- .../components/stat_items/areachart.tsx | 63 +- .../public/components/stat_items/barchart.tsx | 36 +- .../components/stat_items/index.test.tsx | 117 +--- .../public/components/stat_items/index.tsx | 26 +- .../plugins/siem/server/lib/kpi_hosts/mock.ts | 553 +++++++-------- 8 files changed, 719 insertions(+), 736 deletions(-) diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap index edd194ca69043..8cb4f7fe8516a 100644 --- a/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap @@ -185,7 +185,7 @@ exports[`Host Summary Component #getEuiDescriptionList if IP is an empty list in xmlns="http://www.w3.org/2000/svg" > @@ -210,7 +210,7 @@ exports[`Host Summary Component #getEuiDescriptionList if IP is an empty list in xmlns="http://www.w3.org/2000/svg" > 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 index 030e15d04736d..0897f7918b943 100644 --- 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 @@ -104,7 +104,6 @@ export const KpiHostsComponent = pure(({ data, loading }) => { {fieldTitleMapping.map(stat => { let statItemProps: StatItemsProps = { ...stat, - isLoading: loading, key: `kpi-hosts-summary-${stat.description}`, }; 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 index 29a98e532043a..24f1fb0460a09 100644 --- 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 @@ -1,19 +1,243 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Stat Items loading it renders loading icons 1`] = ` - +> + + + +
+ +
+ +
+ HOSTS +
+
+ +
+ + +
+ +
+ + +
+ + +

+ -- + +

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ + + + +`; + +exports[`Stat Items disable charts it renders the default widget 2`] = ` + + + + +
+ +
+ +
+ HOSTS +
+
+ +
+ + +
+ +
+ + +
+ + +

+ -- + +

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ + + + `; exports[`Stat Items rendering kpis with charts it renders the default widget 1`] = ` @@ -83,23 +307,26 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] ] } description="UNIQUE_PRIVATE_IPS" + enableAreaChart={true} + enableBarChart={true} fields={ Array [ Object { "color": "#DB1374", "description": "Source", + "icon": "cross", "key": "uniqueSourceIps", "value": 1714, }, Object { "color": "#490092", "description": "Dest.", + "icon": "cross", "key": "uniqueDestinationIps", "value": 2359, }, ] } - isLoading={false} >
+ + +
+ + + + + + + +
+
+

1,714 @@ -264,10 +548,10 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] key="stat-items-field-uniqueDestinationIps" >

+ + +
+ + + + + + + +
+
+

2,359 @@ -318,10 +656,10 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] >

`; - -exports[`Stat Items rendering kpis without charts it renders the default widget 1`] = ` - - - - -
- -
- -
- HOSTS -
-
- -
- - -
- -
- - -
- - -

- -

-
-
-
-
-
-
-
-
-
-
-
-
- -
- -
- -
- - - - -`; - -exports[`Stat Items rendering kpis without charts it renders the default widget 2`] = ` - - - - -
- -
- -
- HOSTS -
-
- -
- - -
- -
- - -
- - -

- -

-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- - - - -
- - -
- -
- -
- - - - -`; diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 9a8d2402b8944..89ab6ea0ccffd 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -9,56 +9,61 @@ import { pure } from 'recompose'; import styled from 'styled-components'; // @ts-ignore import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; -import { clone as _clone } from 'lodash'; import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.'; import { AutoSizer } from '../auto_sizer'; -const ChartBaseComponent = pure<{ +export const ChartBaseComponent = pure<{ data: AreaChartData[]; width: number | undefined; height: number | undefined; -}>(({ data, ...chartConfigs }) => { - return chartConfigs.width && - chartConfigs.height && - data && - data.length && - data.every(({ value }) => value != null && value.length > 0) ? ( +}>(({ data, ...chartConfigs }) => + chartConfigs.width && chartConfigs.height ? ( + // @ts-ignore - {data.map(series => { - return ( + {data.map(series => + series.value != null ? ( + /** + * Placing ts-ignore here for fillOpacity + * */ // @ts-ignore - ); - })} + ) : null + )} {/* // @ts-ignore */} - value.toString().split('T')[0]} /> + timestamp.split('T')[0]} /> {/* // @ts-ignore */} - ) : ( - - ); -}); + ) : null +); -export const AreaChart = pure<{ areaChart: AreaChartData[] }>(({ areaChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - -)); +export const AreaChart = pure<{ areaChart: AreaChartData[] | [] | null | undefined }>( + ({ areaChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + {areaChart && + areaChart.length && + areaChart.every(({ value }) => value != null && value.length > 0) ? ( + + ) : ( + + )} + + )} + + ) +); -// @ts-ignore 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.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 6548a12e0e1d4..57cb45fe3cee8 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -27,11 +27,8 @@ const ChartBaseComponent = pure<{ width: number | undefined; height: number | undefined; }>(({ data, ...chartConfigs }) => { - return chartConfigs.width && - chartConfigs.height && - data && - data.length && - data.every(({ value }) => value != null && value.length > 0) ? ( + return chartConfigs.width && chartConfigs.height ? ( + // @ts-ignore @@ -56,20 +54,24 @@ const ChartBaseComponent = pure<{ // @ts-ignore */} - ) : ( - - ); + ) : null; }); -export const BarChart = pure<{ barChart: BarChartData[] }>(({ barChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - -)); +export const BarChart = pure<{ barChart: BarChartData[] | [] | null | undefined }>(({ barChart }) => + barChart && + barChart.length && + barChart.every(({ value }) => value != null && value.length > 0) ? ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + + ) : ( + + ) +); // @ts-ignore const SeriesChart = styled(EuiSeriesChart)` 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 index 1343f39865521..bbd3156458434 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -4,43 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow, ReactWrapper } from 'enzyme'; +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, EuiIcon } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiHorizontalRule } from '@elastic/eui'; +// @ts-ignore +import { EuiAreaSeries, EuiBarSeries } from '@elastic/eui/lib/experimental'; describe('Stat Items', () => { - describe('loading', () => { - test('it renders loading icons', () => { - const mockStatItemsData: StatItemsProps = { - fields: [ - { - key: 'networkEvents', - description: 'NETWORK_EVENTS', - value: null, - color: '#000000', - }, - ], - isLoading: true, - key: 'mock-key', - }; - const wrapper = shallow(); - expect(toJson(wrapper)).toMatchSnapshot(); - }); - }); - describe.each([ [ mount( ), @@ -48,26 +29,25 @@ describe('Stat Items', () => { [ mount( ), ], - ])('rendering kpis without charts', wrapper => { + ])('disable charts', wrapper => { test('it renders the default widget', () => { expect(toJson(wrapper)).toMatchSnapshot(); }); - test('should handle multiple titles', () => { - expect(wrapper.find('[data-test-subj="stat-title"]').filter(EuiFlexGroup)).toHaveLength(1); + test('should render titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).toBeTruthy(); }); - test('should not render color indicators', () => { - expect(wrapper.find(EuiIcon)).toHaveLength(0); + test('should not render icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(0); }); test('should not render barChart', () => { @@ -86,9 +66,23 @@ describe('Stat Items', () => { describe('rendering kpis with charts', () => { const mockStatItemsData: StatItemsProps = { fields: [ - { key: 'uniqueSourceIps', description: 'Source', value: 1714, color: '#DB1374' }, - { key: 'uniqueDestinationIps', description: 'Dest.', value: 2359, color: '#490092' }, + { + 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', @@ -118,7 +112,6 @@ describe('Stat Items', () => { }, ], description: 'UNIQUE_PRIVATE_IPS', - isLoading: false, key: 'mock-keys', }; let wrapper: ReactWrapper; @@ -130,11 +123,11 @@ describe('Stat Items', () => { }); test('should handle multiple titles', () => { - expect(wrapper.find('[data-test-subj="stat-title"]').filter(EuiFlexGroup)).toHaveLength(2); + expect(wrapper.find('[data-test-subj="stat-title"]')).toHaveLength(2); }); - test('should render color indicators', () => { - expect(wrapper.find(EuiIcon)).toHaveLength(2); + test('should render kpi icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(2); }); test('should render barChart', () => { @@ -145,55 +138,7 @@ describe('Stat Items', () => { expect(wrapper.find(AreaChart)).toHaveLength(1); }); - test('should render spliter', () => { - expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); - }); - }); - - describe('areaChart data not available', () => { - const mockStatItemsData: StatItemsProps = { - fields: [ - { key: 'uniqueSourceIps', description: 'Source', value: 1714, color: '#DB1374' }, - { key: 'uniqueDestinationIps', description: 'Dest.', value: 2359, color: '#490092' }, - ], - areaChart: [ - { - key: 'uniqueSourceIpsHistogram', - value: [], - color: '#DB1374', - }, - { - key: 'uniqueDestinationIpsHistogram', - value: [], - 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', - isLoading: false, - key: 'mock-keys', - }; - let wrapper: ReactWrapper; - beforeAll(() => { - wrapper = mount(); - }); - - test('should render barChart', () => { - expect(wrapper.find(BarChart)).toHaveLength(1); - }); - - test('should not render areaChart', () => { - expect(wrapper.find(AreaChart)).toHaveLength(0); - }); - - test('should render spliter', () => { + 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 index cf822f39d39e3..bb8fda10664d0 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -42,13 +42,13 @@ export interface StatItem { export interface AreaChartData { key: string; - value: ChartData[] | null; + value: ChartData[] | [] | null; color?: string | undefined; } export interface ChartData { - x: number; - y: number | string; + x: number | string; + y: number; y0?: number; } @@ -67,24 +67,13 @@ export interface StatItems { } export interface StatItemsProps extends StatItems { - isLoading: boolean; key: string; areaChart?: AreaChartData[]; barChart?: BarChartData[]; } export const StatItemsComponent = pure( - ({ - fields, - description, - isLoading, - key, - grow, - barChart, - areaChart, - enableAreaChart, - enableBarChart, - }) => { + ({ fields, description, key, grow, barChart, areaChart, enableAreaChart, enableBarChart }) => { const isBarChartDataAbailable = barChart && barChart.length && @@ -106,7 +95,12 @@ export const StatItemsComponent = pure( {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon ? ( - + ) : null} diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts index b1e978eba3fb1..903c17dbe27d7 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -31,11 +31,11 @@ export const mockRequest = { operationName: 'GetKpiHostsQuery', variables: { sourceId: 'default', - timerange: { interval: '12h', from: 1549765830772, to: 1549852230772 }, + timerange: { interval: '12h', from: 1556890277121, to: 1556976677122 }, filterQuery: '', }, query: - 'query GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery) {\n hosts\n agents\n authentication {\n success\n failure\n __typename\n }\n uniqueSourceIps\n uniqueDestinationIps\n __typename\n }\n __typename\n }\n}\n', + '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: {}, }; @@ -44,12 +44,12 @@ export const mockResponse = { took: 4405, responses: [ { - took: 4404, + took: 1234, timed_out: false, _shards: { total: 71, successful: 71, - skipped: 64, + skipped: 65, failed: 0, }, hits: { @@ -58,93 +58,111 @@ export const mockResponse = { }, aggregations: { unique_destination_ips_histogram: { - doc_count: 2175136, - ips_over_time: { - buckets: [ - { - key_as_string: '2019-04-24T07:00:00.000Z', - key: 1556089200000, - doc_count: 1189146, + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1809, }, - { - key_as_string: '2019-04-24T19:00:00.000Z', - key: 1556132400000, - doc_count: 977334, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 407, }, - { - key_as_string: '2019-04-25T07:00:00.000Z', - key: 1556175600000, - doc_count: 8656, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 64, }, - ], - interval: '12h', - }, + }, + ], + interval: '12h', }, unique_source_ips: { - value: 11929, + value: 1407, }, hosts: { - value: 1026, + value: 986, }, - hosts_histogram: { - doc_count: 9681207, - hosts_over_time: { - buckets: [ - { - key_as_string: '2019-04-24T07:00:00.000Z', - key: 1556089200000, - doc_count: 5017216, + 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-04-24T19:00:00.000Z', - key: 1556132400000, - doc_count: 4590090, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 364, }, - { - key_as_string: '2019-04-25T07:00:00.000Z', - key: 1556175600000, - doc_count: 73901, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 63, }, - ], - interval: '12h', - }, - }, - unique_destination_ips: { - value: 2662, + }, + ], + interval: '12h', }, - unique_source_ips_histogram: { - doc_count: 2503604, - ips_over_time: { - buckets: [ - { - key_as_string: '2019-04-24T07:00:00.000Z', - key: 1556089200000, - doc_count: 1419836, + hosts_histogram: { + buckets: [ + { + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 919, }, - { - key_as_string: '2019-04-24T19:00:00.000Z', - key: 1556132400000, - doc_count: 1074440, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 82, }, - { - key_as_string: '2019-04-25T07:00:00.000Z', - key: 1556175600000, - doc_count: 9328, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 4, }, - ], - interval: '12h', - }, + }, + ], + interval: '12h', + }, + unique_destination_ips: { + value: 1954, }, }, status: 200, }, { - took: 1124, + took: 320, timed_out: false, _shards: { total: 71, successful: 71, - skipped: 64, + skipped: 65, failed: 0, }, hits: { @@ -153,55 +171,68 @@ export const mockResponse = { }, aggregations: { authentication_success: { - doc_count: 2, - attempts_over_time: { - buckets: [ - { - key_as_string: '2019-04-24T18:00:00.000Z', - key: 1556128800000, - doc_count: 1, - }, - { - key_as_string: '2019-04-24T21:00:00.000Z', - key: 1556139600000, - doc_count: 0, + 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-04-25T00:00:00.000Z', - key: 1556150400000, - doc_count: 0, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 3979, }, - { - key_as_string: '2019-04-25T03:00:00.000Z', - key: 1556161200000, - doc_count: 1, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 12, }, - ], - interval: '3h', - }, + }, + ], + interval: '12h', }, - authentication_failure: { - doc_count: 306495, - attempts_over_time: { - buckets: [ - { - key_as_string: '2019-04-24T07:00:00.000Z', - key: 1556089200000, - doc_count: 220265, + 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-04-24T19:00:00.000Z', - key: 1556132400000, - doc_count: 86135, + }, + { + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 52, }, - { - key_as_string: '2019-04-25T07:00:00.000Z', - key: 1556175600000, - doc_count: 95, + }, + { + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 1, }, - ], - interval: '12h', - }, + }, + ], + interval: '12h', }, }, status: 200, @@ -210,99 +241,139 @@ export const mockResponse = { }; export const mockResult = { - hosts: 1026, + hosts: 986, hostsHistogram: [ { - doc_count: 5017216, - key: 1556089200000, - key_as_string: '2019-04-24T07:00:00.000Z', + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 919, + }, }, { - doc_count: 4590090, - key: 1556132400000, - key_as_string: '2019-04-24T19:00:00.000Z', + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 82, + }, }, { - doc_count: 73901, - key: 1556175600000, - key_as_string: '2019-04-25T07:00:00.000Z', + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 4, + }, }, ], - authSuccess: 2, + authSuccess: 61, authSuccessHistogram: [ { - doc_count: 1, - key: 1556128800000, - key_as_string: '2019-04-24T18:00:00.000Z', - }, - { - doc_count: 0, - key: 1556139600000, - key_as_string: '2019-04-24T21:00:00.000Z', + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 8, + }, }, { - doc_count: 0, - key: 1556150400000, - key_as_string: '2019-04-25T00:00:00.000Z', + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 52, + }, }, { - doc_count: 1, - key: 1556161200000, - key_as_string: '2019-04-25T03:00:00.000Z', + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 1, + }, }, ], - authFailure: 306495, + authFailure: 15722, authFailureHistogram: [ { - doc_count: 220265, - key: 1556089200000, - key_as_string: '2019-04-24T07:00:00.000Z', + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 11739, + count: { + doc_count: 11731, + }, }, { - doc_count: 86135, - key: 1556132400000, - key_as_string: '2019-04-24T19:00:00.000Z', + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 4031, + count: { + doc_count: 3979, + }, }, { - doc_count: 95, - key: 1556175600000, - key_as_string: '2019-04-25T07:00:00.000Z', + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 13, + count: { + doc_count: 12, + }, }, ], - uniqueSourceIps: 11929, + uniqueSourceIps: 1407, uniqueSourceIpsHistogram: [ { - doc_count: 1419836, - key: 1556089200000, - key_as_string: '2019-04-24T07:00:00.000Z', + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1182, + }, }, { - doc_count: 1074440, - key: 1556132400000, - key_as_string: '2019-04-24T19:00:00.000Z', + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 364, + }, }, { - doc_count: 9328, - key: 1556175600000, - key_as_string: '2019-04-25T07:00:00.000Z', + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 63, + }, }, ], - uniqueDestinationIps: 2662, + uniqueDestinationIps: 1954, uniqueDestinationIpsHistogram: [ { - doc_count: 1189146, - key: 1556089200000, - key_as_string: '2019-04-24T07:00:00.000Z', + key_as_string: '2019-05-03T13:00:00.000Z', + key: 1556888400000, + doc_count: 3158515, + count: { + value: 1809, + }, }, { - doc_count: 977334, - key: 1556132400000, - key_as_string: '2019-04-24T19:00:00.000Z', + key_as_string: '2019-05-04T01:00:00.000Z', + key: 1556931600000, + doc_count: 703032, + count: { + value: 407, + }, }, { - doc_count: 8656, - key: 1556175600000, - key_as_string: '2019-04-25T07:00:00.000Z', + key_as_string: '2019-05-04T13:00:00.000Z', + key: 1556974800000, + doc_count: 1780, + count: { + value: 64, + }, }, ], }; @@ -315,101 +386,24 @@ export const mockGeneralQuery = [ }, { aggregations: { - hosts: { - cardinality: { - field: 'host.name', - }, - }, + hosts: { cardinality: { field: 'host.name' } }, hosts_histogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - minimum_should_match: 1, - }, - }, - aggregations: { - hosts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: '6', - }, - }, - }, - }, - unique_source_ips: { - cardinality: { - field: 'source.ip', - }, + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'host.name' } } }, }, + unique_source_ips: { cardinality: { field: 'source.ip' } }, unique_source_ips_histogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'source.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - aggregations: { - ips_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, - }, - }, - }, - }, - unique_destination_ips: { - cardinality: { - field: 'destination.ip', - }, + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'source.ip' } } }, }, + unique_destination_ips: { cardinality: { field: 'destination.ip' } }, unique_destination_ips_histogram: { - filter: { - bool: { - should: [ - { - exists: { - field: 'destination.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - aggregations: { - ips_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, - }, - }, - }, + auto_date_histogram: { field: '@timestamp', buckets: '6' }, + aggs: { count: { cardinality: { field: 'destination.ip' } } }, }, }, query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 1556091284295, - lte: 1556177684295, - }, - }, - }, - ], - }, + bool: { filter: [{ range: { '@timestamp': { gte: 1556889840660, lte: 1556976240660 } } }] }, }, size: 0, track_total_hits: false, @@ -424,35 +418,15 @@ export const mockAuthQuery = [ }, { aggs: { - authentication_success: { - filter: { - term: { - 'event.type': 'authentication_success', - }, - }, - aggs: { - attempts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, - }, - }, - }, + 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', - }, - }, - aggs: { - attempts_over_time: { - auto_date_histogram: { - field: '@timestamp', - buckets: 6, - }, - }, - }, + 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: { @@ -461,28 +435,13 @@ export const mockAuthQuery = [ { bool: { should: [ - { - match: { - 'event.type': 'authentication_success', - }, - }, - { - match: { - 'event.type': 'authentication_failure', - }, - }, + { match: { 'event.type': 'authentication_success' } }, + { match: { 'event.type': 'authentication_failure' } }, ], minimum_should_match: 1, }, }, - { - range: { - '@timestamp': { - gte: 1556091284295, - lte: 1556177684295, - }, - }, - }, + { range: { '@timestamp': { gte: 1556889840660, lte: 1556976240660 } } }, ], }, }, From 67bec4d63b0b46a4b42fd044bdce8b63451622b9 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 5 May 2019 17:23:29 +0800 Subject: [PATCH 17/26] handle no data cases --- .../components/page/hosts/kpi_hosts/index.tsx | 20 +++++++++------ .../public/components/stat_items/barchart.tsx | 18 +++++++------ .../public/components/stat_items/index.tsx | 4 +-- .../server/graphql/kpi_hosts/schema.test.ts | 25 +++++++++++++++---- 4 files changed, 45 insertions(+), 22 deletions(-) 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 index 0897f7918b943..0bc6b894c0328 100644 --- 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 @@ -144,17 +144,21 @@ const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartD })); const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { - return fields - .filter(field => get(field.key, data) != null) - .map((field, idx) => { - return { + return fields.reduce((acc: BarChartData[], field: StatItem, idx: number) => { + const key = get('key', field); + const x: number | null = getOr(null, key, data); + const y: string = getOr('', `${idx}.description`, fields); + const dataSet: BarChartData[] = []; + if (y != null) + dataSet.push({ ...field, value: [ { - x: get(field.key, data), - y: getOr('', `${idx}.description`, fields), + x, + y, }, ], - }; - }); + }); + return dataSet; + }, []); }; diff --git a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx index 57cb45fe3cee8..323cf44023e18 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -57,19 +57,23 @@ const ChartBaseComponent = pure<{ ) : null; }); -export const BarChart = pure<{ barChart: BarChartData[] | [] | null | undefined }>(({ barChart }) => - barChart && - barChart.length && - barChart.every(({ value }) => value != null && value.length > 0) ? ( +export const BarChart = pure<{ barChart: BarChartData[] | [] | null | undefined }>( + ({ barChart }) => ( {({ measureRef, content: { height, width } }) => ( - + {barChart && + barChart.length && + barChart.every( + ({ value }) => value != null && value.length > 0 && value.some(({ x }) => x != null) + ) ? ( + + ) : ( + + )} )} - ) : ( - ) ); diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index bb8fda10664d0..a92241b87c6c5 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -47,8 +47,8 @@ export interface AreaChartData { } export interface ChartData { - x: number | string; - y: number; + x: number | string | null; + y: number | string | null; y0?: number; } diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts index 208ca8a3562ec..23459ccabe686 100644 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts @@ -33,27 +33,42 @@ const testKpiHostsSource = { hosts hostsHistogram { key - doc_count + count { + doc_count + value + } } authSuccess authSuccessHistogram { key - doc_count + count { + doc_count + value + } } authFailure authFailureHistogram { key - doc_count + count { + doc_count + value + } } uniqueSourceIps uniqueSourceIpsHistogram { key - doc_count + count { + doc_count + value + } } uniqueDestinationIps uniqueDestinationIpsHistogram { key - doc_count + count { + doc_count + value + } } } } From b9a0cb6066377fde7c6ac2880c5f4f96749e3abb Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Sun, 5 May 2019 20:36:18 +0800 Subject: [PATCH 18/26] update unit test --- .../__snapshots__/index.test.tsx.snap | 4 +- .../components/page/hosts/kpi_hosts/index.tsx | 2 +- .../page/network/kpi_network/index.tsx | 1 - .../components/stat_items/areachart.tsx | 2 +- .../siem/public/graphql/introspection.json | 8 - x-pack/plugins/siem/public/graphql/types.ts | 4 +- .../graphql/kpi_hosts/kpi_hosts.mock.ts | 84 +++++++-- .../server/graphql/kpi_hosts/schema.gql.ts | 1 - x-pack/plugins/siem/server/graphql/types.ts | 9 - .../api_integration/apis/siem/kpi_hosts.ts | 176 +++++++++++++----- 10 files changed, 200 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap index 8cb4f7fe8516a..edd194ca69043 100644 --- a/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/page/hosts/host_summary/__snapshots__/index.test.tsx.snap @@ -185,7 +185,7 @@ exports[`Host Summary Component #getEuiDescriptionList if IP is an empty list in xmlns="http://www.w3.org/2000/svg" > @@ -210,7 +210,7 @@ exports[`Host Summary Component #getEuiDescriptionList if IP is an empty list in xmlns="http://www.w3.org/2000/svg" > 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 index 0bc6b894c0328..5292d99ccf3be 100644 --- 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 @@ -159,6 +159,6 @@ const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartDat }, ], }); - return dataSet; + return acc.concat(dataSet); }, []); }; 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 0cad3183c225c..47b3b086e171d 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 @@ -99,7 +99,6 @@ export const KpiNetworkComponent = pure(({ data, loading }) => {fieldTitleMapping.map(stat => ( diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 89ab6ea0ccffd..14087fda44c95 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -28,7 +28,7 @@ export const ChartBaseComponent = pure<{ // @ts-ignore { key?: KeyResolver; - doc_count?: DocCountResolver; - key_as_string?: KeyAsStringResolver; count?: CountResolver; @@ -5309,11 +5305,6 @@ export namespace HistogramDataResolvers { Parent = HistogramData, Context = SiemContext > = Resolver; - export type DocCountResolver< - R = number | null, - Parent = HistogramData, - Context = SiemContext - > = Resolver; export type KeyAsStringResolver< R = string | null, Parent = HistogramData, diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index fd01b299eb5f0..21f1e5df7778f 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -38,23 +38,39 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.hosts).to.equal(1); expect(kpiHosts!.hostsHistogram).to.eql([ { - x: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, ]); @@ -65,46 +81,78 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueSourceIpsHistogram).to.eql([ { - x: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 52, + }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 31, + }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + 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: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 61, + }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 45, + }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 114, + }, __typename: 'HistogramData', }, ]); @@ -137,23 +185,39 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.hosts).to.equal(1); expect(kpiHosts!.hostsHistogram).to.eql([ { - x: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + x: '2019-02-10T01:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 1, + }, __typename: 'HistogramData', }, ]); @@ -164,46 +228,62 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { expect(kpiHosts!.uniqueSourceIps).to.equal(121); expect(kpiHosts!.uniqueSourceIpsHistogram).to.eql([ { - x: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 52, + }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 0, + }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { + __typename: 'Count', + doc_count: null, + value: 31, + }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + 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: 1549728000000, - y: 1574, + x: '2019-02-09T16:00:00.000Z', + y: { value: 61, doc_count: null, __typename: 'Count' }, __typename: 'HistogramData', }, { - x: 1549738800000, - y: 0, + x: '2019-02-09T19:00:00.000Z', + y: { value: 0, doc_count: null, __typename: 'Count' }, __typename: 'HistogramData', }, { - x: 1549749600000, - y: 1302, + x: '2019-02-09T22:00:00.000Z', + y: { value: 45, doc_count: null, __typename: 'Count' }, __typename: 'HistogramData', }, { - x: 1549760400000, - y: 3281, + x: '2019-02-10T01:00:00.000Z', + y: { value: 114, doc_count: null, __typename: 'Count' }, __typename: 'HistogramData', }, ]); From 3d4dc6eca96b5c16379c0b2195c135b9f588dc47 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 6 May 2019 01:12:05 +0800 Subject: [PATCH 19/26] rename i18n var --- .../siem/public/components/page/hosts/kpi_hosts/index.tsx | 2 +- .../siem/public/components/page/hosts/kpi_hosts/translations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 5292d99ccf3be..58b84afcb8ed0 100644 --- 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 @@ -88,7 +88,7 @@ const fieldTitleMapping: StatItems[] = [ enableAreaChart: true, enableBarChart: true, grow: 4, - description: i18n.UNIQUE_PRIVATE_IPS, + description: i18n.UNIQUE_IPS, }, ]; 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 index cf343f8dfca41..7c4673744eae7 100644 --- 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 @@ -35,7 +35,7 @@ export const ACTIVE_USERS = i18n.translate('xpack.siem.kpiHosts.source.activeUse defaultMessage: 'Active Users', }); -export const UNIQUE_PRIVATE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueIpsTitle', { +export const UNIQUE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniqueIpsTitle', { defaultMessage: 'Unique IPs', }); From e4071780fa20583dcb254ef31a8fcd19c86cacec Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 6 May 2019 02:05:01 +0800 Subject: [PATCH 20/26] fix response data type --- .../lib/kpi_hosts/elasticsearch_adapter.ts | 20 ++-- .../siem/server/lib/kpi_hosts/types.ts | 98 ++++++++++++++++++- 2 files changed, 107 insertions(+), 11 deletions(-) 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 index 9c3d2e25e13b6..57caf40caa2f0 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -12,7 +12,12 @@ import { TermAggregation } from '../types'; import { buildAuthQuery } from './query_authentication.dsl'; import { buildGeneralQuery } from './query_general.dsl'; -import { KpiHostsAdapter, KpiHostsESMSearchBody, KpiHostsHit } from './types'; +import { + KpiHostsAdapter, + KpiHostsESMSearchBody, + KpiHostsGeneralHit, + KpiHostsAuthHit, +} from './types'; export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -23,13 +28,12 @@ export class ElasticsearchKpiHostsAdapter implements KpiHostsAdapter { ): Promise { const generalQuery: KpiHostsESMSearchBody[] = buildGeneralQuery(options); const authQuery: KpiHostsESMSearchBody[] = buildAuthQuery(options); - const response = await this.framework.callWithRequest( - request, - 'msearch', - { - body: [...generalQuery, ...authQuery], - } - ); + 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), diff --git a/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts index db931f4efd066..e744811f243b0 100644 --- a/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/types.ts @@ -11,21 +11,113 @@ export interface KpiHostsAdapter { getKpiHosts(request: FrameworkRequest, options: RequestBasicOptions): Promise; } -export interface KpiHostsHit extends SearchHit { +export interface KpiHostsGeneralHit extends SearchHit { aggregations: { hosts: { value: number; }; - agents: { - 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 { From 7e40378df326ae46dc429b4a4c6316e7de85de39 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 6 May 2019 15:22:57 +0800 Subject: [PATCH 21/26] add unit test for areachart --- .../__snapshots__/index.test.tsx.snap | 92 ++++++++++- .../components/stat_items/areachart.test.tsx | 143 ++++++++++++++++++ .../components/stat_items/areachart.tsx | 33 ++-- .../public/components/stat_items/index.tsx | 4 - 4 files changed, 254 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx 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 index 24f1fb0460a09..1fc8646716536 100644 --- 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 @@ -721,7 +721,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] innerRef={[Function]} >
+ > + + + +
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..76baf76b648a0 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.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 * as React from 'react'; + +import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart'; +import { AreaChartData } from '.'; + +describe('AreaChartBaseComponent', () => { + let wrapper: ReactWrapper; + + 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('renders areachart', () => { + 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]])('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 index 14087fda44c95..1e3e5a8c3f3c4 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -12,14 +12,19 @@ import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/ import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.'; import { AutoSizer } from '../auto_sizer'; -export const ChartBaseComponent = pure<{ +export const AreaChartBaseComponent = pure<{ data: AreaChartData[]; - width: number | undefined; - height: number | undefined; + width: number | null | undefined; + height: number | null | undefined; }>(({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( // @ts-ignore - + {data.map(series => series.value != null ? ( /** @@ -46,18 +51,24 @@ export const ChartBaseComponent = pure<{ ) : null ); +export const AreaChartWithCustomPrompt = pure<{ + data: AreaChartData[] | 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) ? ( + + ) : ( + + ); +}); + export const AreaChart = pure<{ areaChart: AreaChartData[] | [] | null | undefined }>( ({ areaChart }) => ( {({ measureRef, content: { height, width } }) => ( - {areaChart && - areaChart.length && - areaChart.every(({ value }) => value != null && value.length > 0) ? ( - - ) : ( - - )} + )} diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index a92241b87c6c5..e3847144f74a5 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -26,10 +26,6 @@ import { getEmptyTagValue } from '../empty_value'; export const WrappedByAutoSizer = styled.div` height: 100px; position: relative; - - &:hover { - z-index: 100; - } `; export interface StatItem { From 5dae0fac61c0e2badf49b4aa45ae4b7d7020eb60 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 6 May 2019 15:57:57 +0800 Subject: [PATCH 22/26] add unit test for barchart --- .../__snapshots__/index.test.tsx.snap | 56 ++++++++- .../components/stat_items/areachart.test.tsx | 119 +++++++----------- .../components/stat_items/barchart.test.tsx | 86 +++++++++++++ .../public/components/stat_items/barchart.tsx | 29 +++-- 4 files changed, 202 insertions(+), 88 deletions(-) create mode 100644 x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx 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 index 1fc8646716536..f49ded8c03093 100644 --- 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 @@ -774,7 +774,61 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] }, ] } - /> + > + + + +
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 index 76baf76b648a0..9c167f2647a7e 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.test.tsx @@ -12,35 +12,30 @@ 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( - - ); + wrapper = mount(); }); it('should render two area series', () => { @@ -59,30 +54,7 @@ describe('AreaChartBaseComponent', () => { describe('no render', () => { beforeAll(() => { wrapper = mount( - + ); }); @@ -94,33 +66,30 @@ describe('AreaChartBaseComponent', () => { describe('AreaChartWithCustomPrompt', () => { 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('renders areachart', () => { beforeAll(() => { wrapper = mount( - + ); }); @@ -130,7 +99,7 @@ describe('AreaChartWithCustomPrompt', () => { }); }); - describe.each([[], [null]])('renders prompt', (data: AreaChartData[] | [] | null | undefined) => { + describe.each([[], null])('renders prompt', (data: AreaChartData[] | [] | null | undefined) => { beforeAll(() => { wrapper = mount(); }); 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..6739034995134 --- /dev/null +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx @@ -0,0 +1,86 @@ +/* + * 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('BarChartWithCustomPrompt', () => { + 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('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])('renders prompt', (data: BarChartData[] | [] | null | undefined) => { + 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 index 323cf44023e18..878e0229e115f 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -22,10 +22,10 @@ const getYaxis = (value: string | number) => { return label.length > 4 ? `${label.slice(0, 4)}.` : label; }; -const ChartBaseComponent = pure<{ +export const BarChartBaseComponent = pure<{ data: BarChartData[]; - width: number | undefined; - height: number | undefined; + width: number | null | undefined; + height: number | null | undefined; }>(({ data, ...chartConfigs }) => { return chartConfigs.width && chartConfigs.height ? ( // @ts-ignore @@ -33,6 +33,7 @@ const ChartBaseComponent = pure<{ yType={SCALE.ORDINAL} orientation={ORIENTATION.HORIZONTAL} showDefaultAxis={false} + data-test-subj="stat-bar-chart" {...chartConfigs} > {data.map(series => { @@ -57,20 +58,24 @@ const ChartBaseComponent = pure<{ ) : 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) ? ( + + ) : ( + + ); +}); + export const BarChart = pure<{ barChart: BarChartData[] | [] | null | undefined }>( ({ barChart }) => ( {({ measureRef, content: { height, width } }) => ( - {barChart && - barChart.length && - barChart.every( - ({ value }) => value != null && value.length > 0 && value.some(({ x }) => x != null) - ) ? ( - - ) : ( - - )} + )} From 13bdc9219356c2e5667293e1d8350a696a75a778 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Mon, 6 May 2019 23:39:21 +0800 Subject: [PATCH 23/26] fix UI --- .../public/components/page/hosts/kpi_hosts/index.tsx | 4 +++- .../components/page/hosts/kpi_hosts/translations.ts | 7 +++++++ .../stat_items/__snapshots__/index.test.tsx.snap | 4 ++-- .../siem/public/components/stat_items/barchart.tsx | 9 +++++++-- .../plugins/siem/public/components/stat_items/index.tsx | 7 ++++++- 5 files changed, 25 insertions(+), 6 deletions(-) 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 index 58b84afcb8ed0..b1129b7721b9e 100644 --- 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 @@ -72,6 +72,7 @@ const fieldTitleMapping: StatItems[] = [ fields: [ { key: 'uniqueSourceIps', + name: i18n.UNIQUE_SOURCE_IPS_ABBREVIATION, description: i18n.UNIQUE_SOURCE_IPS, value: null, color: euiColorVis2, @@ -144,10 +145,11 @@ const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartD })); const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { + if (fields.length === 0) return []; return fields.reduce((acc: BarChartData[], field: StatItem, idx: number) => { const key = get('key', field); const x: number | null = getOr(null, key, data); - const y: string = getOr('', `${idx}.description`, fields); + const y: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields); const dataSet: BarChartData[] = []; if (y != null) dataSet.push({ 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 index 7c4673744eae7..366f2fabb734d 100644 --- 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 @@ -43,6 +43,13 @@ export const UNIQUE_SOURCE_IPS = i18n.translate('xpack.siem.kpiHosts.source.uniq 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', { 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 index f49ded8c03093..49d1cf0ca5c16 100644 --- 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 @@ -721,7 +721,7 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] innerRef={[Function]} >
{ const label = value.toString(); - return label.length > 4 ? `${label.slice(0, 4)}.` : label; + const labelLength = 4; + return label.length > labelLength ? `${label.slice(0, labelLength)}.` : label; }; export const BarChartBaseComponent = pure<{ @@ -63,7 +64,11 @@ export const BarChartWithCustomPrompt = pure<{ height: number | null | undefined; width: number | null | undefined; }>(({ data, height, width }) => { - return data && data.length && data.every(({ value }) => value != null && value.length > 0) ? ( + return data && + data.length && + data.every( + ({ value }) => value != null && value.length > 0 && value.every(chart => chart.x != null) + ) ? ( ) : ( diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index e3847144f74a5..57bc8f69ef418 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -26,6 +26,10 @@ import { getEmptyTagValue } from '../empty_value'; export const WrappedByAutoSizer = styled.div` height: 100px; position: relative; + + &:hover { + z-index: 100; + } `; export interface StatItem { @@ -34,6 +38,7 @@ export interface StatItem { value: number | undefined | null; color?: string; icon?: 'storage' | 'cross' | 'check' | 'visMapCoordinate'; + name?: string; } export interface AreaChartData { @@ -137,7 +142,7 @@ export const StatItemsComponent = pure( export const ChartHolder = () => ( - + Chart Data Not Available From aae3feb368af10530af1c50412d4148612cc4d34 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 7 May 2019 14:37:19 +0800 Subject: [PATCH 24/26] add more test case for barchart --- .../components/stat_items/barchart.test.tsx | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) 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 index 6739034995134..51f056569c3d9 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx @@ -50,16 +50,35 @@ describe('BarChartBaseComponent', () => { }); }); -describe('BarChartWithCustomPrompt', () => { +describe.each([ + [ + [ + { key: 'uniqueSourceIps', value: [{ x: 1714, 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: 2354, y: 'uniqueDestinationIps' }], + 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; - const mockBarChartData: BarChartData[] = [ - { key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' }, - { - key: 'uniqueDestinationIps', - value: [{ x: 2354, y: 'uniqueDestinationIps' }], - color: '#490092', - }, - ]; describe('renders barchart', () => { beforeAll(() => { wrapper = mount( @@ -73,7 +92,20 @@ describe('BarChartWithCustomPrompt', () => { }); }); - describe.each([[], null])('renders prompt', (data: BarChartData[] | [] | null | undefined) => { + describe.each([ + [], + null, + [ + [ + { key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + value: [{ x: null, y: 'uniqueDestinationIps' }], + color: '#490092', + }, + ], + ], + ])('renders prompt', (data: BarChartData[] | [] | null | undefined) => { beforeAll(() => { wrapper = mount(); }); From 601b73b11f0769d3388bfcdce868ff768a6ab0d9 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 9 May 2019 05:59:39 +0800 Subject: [PATCH 25/26] fix for code review --- .../components/page/hosts/kpi_hosts/index.tsx | 12 +- .../page/network/kpi_network/index.tsx | 12 +- .../__snapshots__/index.test.tsx.snap | 77 ++++----- .../components/stat_items/areachart.test.tsx | 136 ++++++++++++--- .../components/stat_items/areachart.tsx | 41 ++--- .../components/stat_items/barchart.test.tsx | 86 +++++++--- .../public/components/stat_items/barchart.tsx | 23 +-- .../components/stat_items/index.test.tsx | 2 - .../public/components/stat_items/index.tsx | 44 ++--- .../graphql/kpi_hosts/kpi_hosts.mock.ts | 157 ------------------ .../graphql/kpi_hosts/resolvers.test.ts | 80 --------- .../server/graphql/kpi_hosts/schema.test.ts | 145 ---------------- 12 files changed, 285 insertions(+), 530 deletions(-) delete mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts delete mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts delete mode 100644 x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts 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 index b1129b7721b9e..014d7d5b283b2 100644 --- 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 @@ -147,12 +147,12 @@ const addValueToAreaChart = (fields: StatItem[], data: KpiHostsData): AreaChartD const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartData[] => { if (fields.length === 0) return []; return fields.reduce((acc: BarChartData[], field: StatItem, idx: number) => { - const key = get('key', field); + 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); - const dataSet: BarChartData[] = []; - if (y != null) - dataSet.push({ + + return acc.concat([ + { ...field, value: [ { @@ -160,7 +160,7 @@ const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartDat y, }, ], - }); - return acc.concat(dataSet); + }, + ]); }, []); }; 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 47b3b086e171d..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,8 +9,8 @@ import { get } from 'lodash/fp'; import React from 'react'; import { pure } from 'recompose'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import styled from 'styled-components'; import { StatItem, StatItems, StatItemsComponent } from '../../../../components/stat_items'; import { KpiNetworkData } from '../../../../graphql/types'; @@ -87,13 +87,17 @@ const fieldTitleMapping: Readonly = [ }, ]; +const FlexGroup = styled(EuiFlexGroup)` + margin-height: 86px; +`; + export const KpiNetworkComponent = pure(({ data, loading }) => { return loading ? ( - + - + ) : ( {fieldTitleMapping.map(stat => ( 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 index 49d1cf0ca5c16..e2740c7d16ed9 100644 --- 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 @@ -31,10 +31,10 @@ exports[`Stat Items disable charts it renders the default widget 1`] = ` key="stat-items-undefined" >

-- @@ -153,10 +153,10 @@ exports[`Stat Items disable charts it renders the default widget 2`] = ` key="stat-items-undefined" >

+ 0

-- @@ -419,10 +420,10 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] key="stat-items-undefined" >

1,714 @@ -548,10 +549,10 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] key="stat-items-field-uniqueDestinationIps" >

2,359 @@ -656,10 +657,10 @@ exports[`Stat Items rendering kpis with charts it renders the default widget 1`] >

{ describe('AreaChartWithCustomPrompt', () => { let wrapper: ReactWrapper; - const mockAreaChartData: AreaChartData[] = [ - { - key: 'uniqueSourceIpsHistogram', - value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: 1096175 }, - { x: 1556773200000, y: 12382 }, + 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', + }, ], - color: '#DB1374', - }, - { - key: 'uniqueDestinationIpsHistogram', - value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + [ + [ + { + key: 'uniqueSourceIpsHistogram', + value: [], + color: '#DB1374', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: 1556686800000, y: 565975 }, + { x: 1556730000000, y: 1084366 }, + { x: 1556773200000, y: 12280 }, + ], + color: '#490092', + }, + ], ], - color: '#490092', - }, - ]; - describe('renders areachart', () => { + ], + ])('renders areachart', (data: AreaChartData[] | [] | null | undefined) => { beforeAll(() => { - wrapper = mount( - - ); + wrapper = mount(); }); it('render AreaChartBaseComponent', () => { @@ -99,7 +118,78 @@ describe('AreaChartWithCustomPrompt', () => { }); }); - describe.each([[], null])('renders prompt', (data: AreaChartData[] | [] | null | undefined) => { + 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(); }); diff --git a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx index 1e3e5a8c3f3c4..3dc6782f704f5 100644 --- a/x-pack/plugins/siem/public/components/stat_items/areachart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/areachart.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; -// @ts-ignore import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.'; import { AutoSizer } from '../auto_sizer'; @@ -18,29 +17,26 @@ export const AreaChartBaseComponent = pure<{ height: number | null | undefined; }>(({ data, ...chartConfigs }) => chartConfigs.width && chartConfigs.height ? ( - // @ts-ignore - {data.map(series => - series.value != null ? ( - /** - * Placing ts-ignore here for fillOpacity - * */ + {data.map(series => ( + /** + * Placing ts-ignore here for fillOpacity + * */ + // @ts-ignore + - ) : null - )} + data={series.value} + fillOpacity={0.04} + color={series.color} + /> + ))} {/* // @ts-ignore */} timestamp.split('T')[0]} /> @@ -56,14 +52,21 @@ export const AreaChartWithCustomPrompt = pure<{ height: number | null | undefined; width: number | null | undefined; }>(({ data, height, width }) => { - return data && data.length && data.every(({ value }) => value != null && value.length > 0) ? ( + 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 }>( +export const AreaChart = pure<{ areaChart: AreaChartData[] | null | undefined }>( ({ areaChart }) => ( {({ measureRef, content: { height, width } }) => ( 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 index 51f056569c3d9..3edcb316526e7 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.test.tsx @@ -60,14 +60,18 @@ describe.each([ color: '#490092', }, ], + ], + [ [ - { key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' }, + { key: 'uniqueSourceIps', value: [{ x: 1714, y: '' }], color: '#DB1374' }, { key: 'uniqueDestinationIps', - value: [{ x: 2354, y: 'uniqueDestinationIps' }], + value: [{ x: 2354, y: '' }], color: '#490092', }, ], + ], + [ [ { key: 'uniqueSourceIps', value: [{ x: 0, y: 'uniqueSourceIps' }], color: '#DB1374' }, { @@ -91,28 +95,68 @@ describe.each([ expect(wrapper.find('ChartHolder')).toHaveLength(0); }); }); +}); - describe.each([ - [], - null, +describe.each([ + [], + null, + [ [ - [ - { key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' }, - { - key: 'uniqueDestinationIps', - value: [{ x: null, y: 'uniqueDestinationIps' }], - color: '#490092', - }, - ], + { key: 'uniqueSourceIps', color: '#DB1374' }, + { + key: 'uniqueDestinationIps', + color: '#490092', + }, ], - ])('renders prompt', (data: BarChartData[] | [] | null | undefined) => { - beforeAll(() => { - wrapper = mount(); - }); + ], + [ + [ + { 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); - }); + 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 index 2029def327ce6..10987e0b44449 100644 --- a/x-pack/plugins/siem/public/components/stat_items/barchart.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/barchart.tsx @@ -11,7 +11,6 @@ import { } from '@elastic/eui'; import { pure } from 'recompose'; import styled from 'styled-components'; -// @ts-ignore import { EuiSeriesChart, EuiBarSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental'; import { BarChartData, WrappedByAutoSizer, ChartHolder } from '.'; import { AutoSizer } from '../auto_sizer'; @@ -39,7 +38,6 @@ export const BarChartBaseComponent = pure<{ > {data.map(series => { return ( - // @ts-ignore ( - ({ barChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - - ) -); +export const BarChart = pure<{ barChart: BarChartData[] | null | undefined }>(({ barChart }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + +)); -// @ts-ignore const SeriesChart = styled(EuiSeriesChart)` svg .rv-xy-plot__axis--horizontal 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 index bbd3156458434..39b8b15c39b48 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -12,8 +12,6 @@ import { StatItemsComponent, StatItemsProps } from '.'; import { BarChart } from './barchart'; import { AreaChart } from './areachart'; import { EuiHorizontalRule } from '@elastic/eui'; -// @ts-ignore -import { EuiAreaSeries, EuiBarSeries } from '@elastic/eui/lib/experimental'; describe('Stat Items', () => { describe.each([ diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index 57bc8f69ef418..ac4bf34bf2f6e 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -9,8 +9,6 @@ import { EuiFlexItem, EuiPanel, EuiHorizontalRule, - // @ts-ignore - EuiStat, EuiIcon, EuiTitle, } from '@elastic/eui'; @@ -32,6 +30,20 @@ export const WrappedByAutoSizer = styled.div` } `; +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; @@ -94,7 +106,7 @@ export const StatItemsComponent = pure( {fields.map(field => ( - {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon ? ( + {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon && ( ( data-test-subj="stat-icon" /> - ) : null} + )} @@ -118,20 +130,20 @@ export const StatItemsComponent = pure( ))} - {enableAreaChart || enableBarChart ? : null} + {(enableAreaChart || enableBarChart) && } - {enableBarChart ? ( + {enableBarChart && ( - ) : null} + )} - {enableAreaChart ? ( + {enableAreaChart && ( - ) : null} + )} @@ -140,21 +152,11 @@ export const StatItemsComponent = pure( ); export const ChartHolder = () => ( - + Chart Data Not Available - + ); - -const FlexItem = styled(EuiFlexItem)` - min-width: 0; -`; - -const StatValue = styled(EuiTitle)` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts deleted file mode 100644 index b89891f859a38..0000000000000 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/kpi_hosts.mock.ts +++ /dev/null @@ -1,157 +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 { Logger } from '../../utils/logger'; -import { SiemContext } from '../index'; -import { KpiHostsData } from '../types'; - -export const mockKpiHostsData: { KpiHosts: KpiHostsData } = { - KpiHosts: { - hosts: 1026, - hostsHistogram: [ - { - count: { - doc_count: 91.01353874452269, - value: 84.36392955408016, - }, - key: 1556089200000, - }, - { - count: { - doc_count: -71.23554555913962, - value: -86.81571698230019, - }, - key: 1556132400000, - }, - { - count: { - doc_count: -46.879996993392936, - value: 20.02705123314729, - }, - key: 1556175600000, - }, - ], - authSuccess: 2, - authSuccessHistogram: [ - { - count: { - doc_count: -65.55778706419551, - value: 67.95125899290869, - }, - key: 1556128800000, - }, - { - count: { - doc_count: 92.17135402067896, - value: -16.86811977391112, - }, - key: 1556139600000, - }, - { - count: { - doc_count: -94.72718497628212, - value: 89.51615311876893, - }, - key: 1556150400000, - }, - { - count: { - doc_count: 32.72219362130082, - value: 1.473185911562652, - }, - key: 1556161200000, - }, - ], - authFailure: 306495, - authFailureHistogram: [ - { - count: { - doc_count: 22.67887099679365, - value: 71.72317515961674, - }, - key: 54.78386776414678, - }, - { - count: { - doc_count: 19.546134098311626, - value: 24.07834605598333, - }, - key: -88.10762445863057, - }, - { - count: { - doc_count: -93.9609610318457, - value: 86.81313898837192, - }, - key: 1556175600000, - }, - ], - uniqueSourceIps: 11929, - uniqueSourceIpsHistogram: [ - { - count: { - doc_count: -93.65873039980133, - value: 35.87162669927895, - }, - key: 1556089200000, - }, - { - count: { - doc_count: -1.0873234537593532, - value: -10.889541449439605, - }, - key: 1556132400000, - }, - { - count: { - doc_count: -29.624594846902227, - value: -7.672285811211083, - }, - key: 1556175600000, - }, - ], - uniqueDestinationIps: 2662, - uniqueDestinationIpsHistogram: [ - { - count: { - doc_count: 53.48312028233991, - value: 53.28368150914008, - }, - key: 1556089200000, - }, - { - count: { - doc_count: -18.265149209717222, - value: 98.66572548343012, - }, - key: 1556132400000, - }, - { - count: { - doc_count: 12.442923157321715, - value: 57.449121360985515, - }, - key: 1556175600000, - }, - ], - }, -}; - -export const getKpiHostsQueryMock = (logger: Logger) => ({ - source: (root: unknown, args: unknown, context: SiemContext) => { - logger.info('Mock source'); - const operationName = context.req.payload.operationName.toLowerCase(); - switch (operationName) { - case 'test': { - logger.info(`Using mock for test ${mockKpiHostsData}`); - return mockKpiHostsData; - } - default: { - return {}; - } - } - }, -}); diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts deleted file mode 100644 index 2858e43a0e583..0000000000000 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/resolvers.test.ts +++ /dev/null @@ -1,80 +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 { GraphQLResolveInfo } from 'graphql'; - -import { Source } from '../../graphql/types'; -import { FrameworkRequest, internalFrameworkRequest } from '../../lib/framework'; -import { KpiHosts } from '../../lib/kpi_hosts'; -import { KpiHostsAdapter } from '../../lib/kpi_hosts/types'; -import { SourceStatus } from '../../lib/source_status'; -import { Sources } from '../../lib/sources'; -import { createSourcesResolvers } from '../sources'; -import { SourcesResolversDeps } from '../sources/resolvers'; -import { mockSourcesAdapter, mockSourceStatusAdapter } from '../sources/resolvers.test'; - -import { mockKpiHostsData } from './kpi_hosts.mock'; -import { createKpiHostsResolvers, KpiHostsResolversDeps } from './resolvers'; - -const mockGetKpiHosts = jest.fn(); -mockGetKpiHosts.mockResolvedValue({ - KpiHosts: { - ...mockKpiHostsData.KpiHosts, - }, -}); -const mockKpiHostsAdapter: KpiHostsAdapter = { - getKpiHosts: mockGetKpiHosts, -}; - -const mockKpiHostsLibs: KpiHostsResolversDeps = { - kpiHosts: new KpiHosts(mockKpiHostsAdapter), -}; - -const mockSrcLibs: SourcesResolversDeps = { - sources: new Sources(mockSourcesAdapter), - sourceStatus: new SourceStatus(mockSourceStatusAdapter, new Sources(mockSourcesAdapter)), -}; - -const req: FrameworkRequest = { - [internalFrameworkRequest]: { - params: {}, - query: {}, - payload: { - operationName: 'test', - }, - }, - params: {}, - query: {}, - payload: { - operationName: 'test', - }, -}; - -const context = { req }; - -describe('Test Source Resolvers', () => { - test('Make sure that getKpiHosts have been called', async () => { - const source = await createSourcesResolvers(mockSrcLibs).Query.source( - {}, - { id: 'default' }, - context, - {} as GraphQLResolveInfo - ); - const data = await createKpiHostsResolvers(mockKpiHostsLibs).Source.KpiHosts( - source as Source, - { - timerange: { - interval: '12h', - to: 1514782800000, - from: 1546318799999, - }, - }, - context, - {} as GraphQLResolveInfo - ); - expect(mockKpiHostsAdapter.getKpiHosts).toHaveBeenCalled(); - expect(data).toEqual(mockKpiHostsData); - }); -}); diff --git a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts deleted file mode 100644 index 23459ccabe686..0000000000000 --- a/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.test.ts +++ /dev/null @@ -1,145 +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 { graphql } from 'graphql'; -import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; - -import { rootSchema } from '../../../common/graphql/root/schema.gql'; -import { sharedSchema } from '../../../common/graphql/shared'; -import { Logger } from '../../utils/logger'; -import { ecsSchema } from '../ecs'; -import { dateSchema } from '../scalar_date'; -import { toBooleanSchema } from '../scalar_to_boolean_array'; -import { toDateSchema } from '../scalar_to_date_array'; -import { toNumberSchema } from '../scalar_to_number_array'; -import { sourceStatusSchema } from '../source_status/schema.gql'; -import { sourcesSchema } from '../sources/schema.gql'; - -import { getKpiHostsQueryMock, mockKpiHostsData } from './kpi_hosts.mock'; -import { kpiHostsSchema } from './schema.gql'; - -const testKpiHostsSource = { - id: 'Test case to query Siem Hosts KPIs data', - query: ` - query GetOverviewHostsQuery( - $timerange: TimerangeInput! - $filterQuery: String - ) { - source(id: "default") { - KpiHosts(timerange: $timerange, filterQuery: $filterQuery) { - hosts - hostsHistogram { - key - count { - doc_count - value - } - } - authSuccess - authSuccessHistogram { - key - count { - doc_count - value - } - } - authFailure - authFailureHistogram { - key - count { - doc_count - value - } - } - uniqueSourceIps - uniqueSourceIpsHistogram { - key - count { - doc_count - value - } - } - uniqueDestinationIps - uniqueDestinationIpsHistogram { - key - count { - doc_count - value - } - } - } - } - } - `, - variables: { - timerange: { - interval: '12h', - to: 1514782800000, - from: 1546318799999, - }, - }, - context: { - req: { - payload: { - operationName: 'test', - }, - }, - }, - expected: { - data: { - source: { - ...mockKpiHostsData, - }, - }, - }, -}; - -describe('SIEM Hosts GQL Schema', () => { - describe('Test KPI Hosts Schema', () => { - // Array of case types - const cases = [testKpiHostsSource]; - const typeDefs = [ - rootSchema, - sharedSchema, - sourcesSchema, - sourceStatusSchema, - ecsSchema, - kpiHostsSchema, - dateSchema, - toNumberSchema, - toDateSchema, - toBooleanSchema, - ]; - const mockSchema = makeExecutableSchema({ typeDefs }); - - // Here we specify the return payloads of mocked types - const logger: Logger = { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - const mocks = { - Query: () => ({ - ...getKpiHostsQueryMock(logger), - }), - }; - - addMockFunctionsToSchema({ - schema: mockSchema, - mocks, - }); - - cases.forEach(obj => { - const { id, query, variables, context, expected } = obj; - - test(`${id}`, async () => { - const result = await graphql(mockSchema, query, null, context, variables); - return await expect(result).toEqual(expected); - }); - }); - }); -}); From a07aaf26c74cbeaec1670d065867bb96aa72e9cd Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 9 May 2019 14:40:06 +0800 Subject: [PATCH 26/26] gathering mock data in kpi_host integration test --- .../api_integration/apis/siem/kpi_hosts.ts | 583 +++++++++++------- 1 file changed, 355 insertions(+), 228 deletions(-) diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 21f1e5df7778f..44c46d01aa0ac 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -19,6 +19,130 @@ const kpiHostsTests: 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: '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 @@ -35,127 +159,7 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }) .then(resp => { const kpiHosts = resp.data.source.KpiHosts; - expect(kpiHosts!.hosts).to.equal(1); - 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: { - __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', - }, - ]); + expect(kpiHosts!).to.eql(expectedResult); }); }); }); @@ -166,7 +170,130 @@ const kpiHostsTests: 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: '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({ @@ -182,116 +309,116 @@ const kpiHostsTests: KbnTestProvider = ({ getService }) => { }) .then(resp => { const kpiHosts = resp.data.source.KpiHosts; - expect(kpiHosts!.hosts).to.equal(1); - 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', - }, - ]); + 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;