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 2ae25e9167a1b..1b50798fea32f 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 './events_table'; export * from './hosts_table'; export * from './types_bar'; 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..441a4b82d8f12 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx @@ -0,0 +1,149 @@ +/* + * 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 fieldTitleMapping: Readonly = [ + { + 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: 'authenticationSuccess', + description: i18n.AUTHENTICATION_SUCCESS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'authenticationFailure', + description: i18n.AUTHENTICATION_FAILURE, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'fimEvents', + description: i18n.FIM_EVENTS, + value: null, + }, + ], + }, + { + fields: [ + { + key: 'auditdEvents', + description: i18n.AUDITD_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 ( + + {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..744916afa168b --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts @@ -0,0 +1,69 @@ +/* + * 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_SUCCESS = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationSuccessTitle', + { + defaultMessage: 'Authentication Success', + } +); + +export const AUTHENTICATION_FAILURE = i18n.translate( + 'xpack.siem.kpiHosts.source.authenticationFailureTitle', + { + defaultMessage: 'Authentication Failure', + } +); + +export const FIM_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.fimEventsTitle', { + defaultMessage: 'Auditbeat FIM Events', +}); + +export const AUDITD_EVENTS = i18n.translate('xpack.siem.kpiHosts.source.auditEventsTitle', { + defaultMessage: 'Auditbeat Auditd 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..a8c17e34782cb --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts @@ -0,0 +1,29 @@ +/* + * 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 + authenticationSuccess + authenticationFailure + fimEvents + auditdEvents + 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 12cac8bb09ef5..9d9b2654be04b 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -396,6 +396,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", @@ -4672,6 +4703,113 @@ "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": "authenticationSuccess", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationFailure", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fimEvents", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "auditdEvents", + "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": "ENUM", "name": "NetworkTopNFlowDirection", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index 443b3bd8fa6e4..cccbbe8c46e4b 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -51,6 +51,8 @@ export interface Source { IpOverview?: IpOverviewData | null; 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; @@ -853,6 +855,32 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + installedPackages?: number | null; + + processCount?: number | null; + + authenticationSuccess?: number | null; + + authenticationFailure?: number | null; + + fimEvents?: number | null; + + auditdEvents?: number | null; + + winlogbeatEvents?: number | null; + + filebeatEvents?: number | null; + + sockets?: number | null; + + uniqueSourceIps?: number | null; + + uniqueDestinationIps?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1097,6 +1125,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { direction: NetworkTopNFlowDirection; @@ -1928,6 +1963,56 @@ export namespace GetKpiEventsQuery { }; } +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; + + authenticationSuccess?: number | null; + + authenticationFailure?: number | null; + + fimEvents?: number | null; + + auditdEvents?: 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 b3dfd1a98dca0..b3c4d90ce2381 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiSpacer } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React from 'react'; import { connect } from 'react-redux'; @@ -11,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 +40,7 @@ const AuthenticationTableManage = manageQuery(AuthenticationTable); const HostsTableManage = manageQuery(HostsTable); const EventsTableManage = manageQuery(EventsTable); const UncommonProcessTableManage = manageQuery(UncommonProcessTable); - +const KpiHostsComponentManage = manageQuery(KpiHostsComponent); interface HostsComponentReduxProps { filterQuery: string; } @@ -51,6 +58,24 @@ const HostsComponent = pure(({ filterQuery }) => ( {({ poll, 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..8a207db6430b7 --- /dev/null +++ b/x-pack/plugins/siem/server/graphql/kpi_hosts/schema.gql.ts @@ -0,0 +1,28 @@ +/* + * 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 + authenticationSuccess: Float + authenticationFailure: Float + fimEvents: Float + auditdEvents: 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 28bdf89b1a763..854684a5e1b71 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -80,6 +80,8 @@ export interface Source { IpOverview?: IpOverviewData | null; 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; @@ -882,6 +884,32 @@ export interface KpiNetworkData { tlsHandshakes?: number | null; } +export interface KpiHostsData { + hosts?: number | null; + + installedPackages?: number | null; + + processCount?: number | null; + + authenticationSuccess?: number | null; + + authenticationFailure?: number | null; + + fimEvents?: number | null; + + auditdEvents?: number | null; + + winlogbeatEvents?: number | null; + + filebeatEvents?: number | null; + + sockets?: number | null; + + uniqueSourceIps?: number | null; + + uniqueDestinationIps?: number | null; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1126,6 +1154,13 @@ export interface KpiNetworkSourceArgs { filterQuery?: string | null; } +export interface KpiHostsSourceArgs { + id?: string | null; + + timerange: TimerangeInput; + + filterQuery?: string | null; +} export interface NetworkTopNFlowSourceArgs { direction: NetworkTopNFlowDirection; @@ -1294,6 +1329,8 @@ export namespace SourceResolvers { IpOverview?: IpOverviewResolver; 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; @@ -1423,6 +1460,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, @@ -4136,6 +4186,95 @@ export namespace KpiNetworkDataResolvers { > = Resolver; } +export namespace KpiHostsDataResolvers { + export interface Resolvers { + hosts?: HostsResolver; + + installedPackages?: InstalledPackagesResolver; + + processCount?: ProcessCountResolver; + + authenticationSuccess?: AuthenticationSuccessResolver; + + authenticationFailure?: AuthenticationFailureResolver; + + fimEvents?: FimEventsResolver; + + auditdEvents?: AuditdEventsResolver; + + 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 AuthenticationSuccessResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuthenticationFailureResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type FimEventsResolver< + R = number | null, + Parent = KpiHostsData, + Context = SiemContext + > = Resolver; + export type AuditdEventsResolver< + 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 e1f2a8604b347..b221a98773411 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 { createIpOverviewResolvers } from './graphql/ip_overview'; +import { createKpiHostsResolvers } from './graphql/kpi_hosts'; import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; import { createOverviewResolvers } from './graphql/overview'; @@ -46,6 +47,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 2fc5ef172a5ed..b8a9c69702868 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, IpOverview } from '../ip_overview'; import { KpiNetwork } from '../kpi_network'; @@ -35,6 +38,7 @@ export function compose(server: Server): AppBackendLibs { events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework), sources), hosts: new Hosts(new ElasticsearchHostsAdapter(framework)), + kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), ipOverview: new IpOverview(new ElasticsearchIpOverviewAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(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..8062e700e3e77 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -0,0 +1,145 @@ +/* + * 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({ + auditdEvents: null, + authenticationFailure: null, + authenticationSuccess: null, + filebeatEvents: null, + fimEvents: 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..9613867949749 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } 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 auditbeatFIMQuery: KpiHostsESMSearchBody[] = buildEventQuery( + { agentType: 'auditbeat', eventModule: 'file_integrity' }, + options + ); + const auditbeatAuditdQuery: KpiHostsESMSearchBody[] = buildEventQuery( + { agentType: 'auditbeat', eventModule: 'auditd' }, + 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, + ...auditbeatFIMQuery, + ...auditbeatAuditdQuery, + ...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), + authenticationSuccess: getOr( + null, + 'responses.2.aggregations.authentication_success.doc_count', + response + ), + authenticationFailure: getOr( + null, + 'responses.2.aggregations.authentication_failure.doc_count', + response + ), + fimEvents: getOr(null, 'responses.3.hits.total.value', response), + auditdEvents: getOr(null, 'responses.4.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 + ), + }; + } +} 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..146b93f0b142e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -0,0 +1,224 @@ +/* + * 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 authenticationSuccess\n authenticationFailure\n fimEvents\n auditdEvents\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 = { + auditdEvents: 0, + authenticationFailure: 0, + authenticationSuccess: 27, + filebeatEvents: 956933, + fimEvents: 0, + 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..636fc2b373db4 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/kpi_hosts/query_event.dsl.ts @@ -0,0 +1,96 @@ +/* + * 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 = ({ eventModule }: EventModuleAttributeQuery) => + eventModule + ? [ + { + bool: { + should: [ + { + match_phrase: { + 'event.module': eventModule, + }, + }, + ], + 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 21430fcadb0ab..88a83f5b08e3b 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 { IpOverview } from './ip_overview'; +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 6886b256a5bf2..0be30c1f5f06f 100644 --- a/x-pack/test/api_integration/apis/siem/index.js +++ b/x-pack/test/api_integration/apis/siem/index.js @@ -16,6 +16,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./timeline')); loadTestFile(require.resolve('./timeline_details')); loadTestFile(require.resolve('./uncommon_processes')); + loadTestFile(require.resolve('./kpi_hosts')); loadTestFile(require.resolve('./kpi_network')); loadTestFile(require.resolve('./overview_network')); 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..96e72c479dbee --- /dev/null +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -0,0 +1,96 @@ +/* + * 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!.authenticationSuccess).to.equal(0); + expect(kpiHosts!.authenticationFailure).to.equal(0); + expect(kpiHosts!.fimEvents).to.equal(0); + expect(kpiHosts!.auditdEvents).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!.authenticationSuccess).to.equal(0); + expect(kpiHosts!.authenticationFailure).to.equal(0); + expect(kpiHosts!.fimEvents).to.equal(0); + expect(kpiHosts!.auditdEvents).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;