From d932830218c00c6548957c0954fcc4ae550352cb Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 2 Sep 2020 15:34:49 +0100 Subject: [PATCH] [Security Solution] Search strategy - authentications (#76090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move authentications to searchstrategy * useAuthentications * add authenticationFields * remove import from lib * remove duplicated types * getInspectResponse * getInspectResponse * move folder * update path * update path * shallowEqual * revert path * update path * remove redundant types * fix import * fix types Co-authored-by: Elastic Machine Co-authored-by: Patryk KopyciƄski --- .../common/ecs/ecs_fields/extend_map.test.ts | 56 +++ .../common/ecs/ecs_fields/extend_map.ts | 14 + .../common/ecs/ecs_fields/index.ts | 358 ++++++++++++++++++ .../hosts/authentications/index.ts | 83 ++++ .../security_solution/hosts/index.ts | 3 +- .../security_solution/index.ts | 50 ++- .../security_solution/public/helpers.ts | 13 + .../authentications_table/index.tsx | 8 +- .../components/authentications_table/mock.ts | 29 +- .../containers/authentications/index.tsx | 294 ++++++++------ .../authentications/translations.ts | 21 + .../public/hosts/containers/hosts/index.tsx | 6 +- .../containers/hosts/overview/_index.tsx | 6 +- .../hosts/containers/hosts/translations.ts | 14 + .../authentications_query_tab_body.tsx | 58 +-- .../network/containers/network_http/index.tsx | 6 +- .../public/network/containers/tls/index.tsx | 4 +- .../plugins/security_solution/public/types.ts | 3 + .../search_strategy/helpers/to_array.ts | 8 + .../hosts/authentications/dsl/query.dsl.ts | 120 ++++++ .../factory/hosts/authentications/helpers.ts | 91 +++++ .../factory/hosts/authentications/index.tsx | 64 ++++ .../factory/hosts/helpers.ts | 8 +- .../security_solution/factory/hosts/index.ts | 2 + .../factory/network/http/index.ts | 1 - 25 files changed, 1143 insertions(+), 177 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts create mode 100644 x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts create mode 100644 x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts new file mode 100644 index 0000000000000..9ba22e83b4b4d --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { extendMap } from './extend_map'; + +describe('ecs_fields test', () => { + describe('extendMap', () => { + test('it should extend a record', () => { + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + expect(extendMap('host', osFieldsMap)).toEqual(expected); + }); + + test('it should extend a sample hosts record', () => { + const hostMap: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + }; + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + const output = { ...hostMap, ...extendMap('host', osFieldsMap) }; + expect(output).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts new file mode 100644 index 0000000000000..c25979cbcdcee --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export const extendMap = ( + path: string, + map: Readonly> +): Readonly> => + Object.entries(map).reduce>((accum, [key, value]) => { + accum[`${path}.${key}`] = `${path}.${value}`; + return accum; + }, {}); diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts new file mode 100644 index 0000000000000..19b16bd4bc6d2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts @@ -0,0 +1,358 @@ +/* + * 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 { extendMap } from './extend_map'; + +export const auditdMap: Readonly> = { + 'auditd.result': 'auditd.result', + 'auditd.session': 'auditd.session', + 'auditd.data.acct': 'auditd.data.acct', + 'auditd.data.terminal': 'auditd.data.terminal', + 'auditd.data.op': 'auditd.data.op', + 'auditd.summary.actor.primary': 'auditd.summary.actor.primary', + 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary', + 'auditd.summary.object.primary': 'auditd.summary.object.primary', + 'auditd.summary.object.secondary': 'auditd.summary.object.secondary', + 'auditd.summary.object.type': 'auditd.summary.object.type', + 'auditd.summary.how': 'auditd.summary.how', + 'auditd.summary.message_type': 'auditd.summary.message_type', + 'auditd.summary.sequence': 'auditd.summary.sequence', +}; + +export const cloudFieldsMap: Readonly> = { + 'cloud.account.id': 'cloud.account.id', + 'cloud.availability_zone': 'cloud.availability_zone', + 'cloud.instance.id': 'cloud.instance.id', + 'cloud.instance.name': 'cloud.instance.name', + 'cloud.machine.type': 'cloud.machine.type', + 'cloud.provider': 'cloud.provider', + 'cloud.region': 'cloud.region', +}; + +export const fileMap: Readonly> = { + 'file.name': 'file.name', + 'file.path': 'file.path', + 'file.target_path': 'file.target_path', + 'file.extension': 'file.extension', + 'file.type': 'file.type', + 'file.device': 'file.device', + 'file.inode': 'file.inode', + 'file.uid': 'file.uid', + 'file.owner': 'file.owner', + 'file.gid': 'file.gid', + 'file.group': 'file.group', + 'file.mode': 'file.mode', + 'file.size': 'file.size', + 'file.mtime': 'file.mtime', + 'file.ctime': 'file.ctime', +}; + +export const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.name': 'os.name', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', +}; + +export const hostFieldsMap: Readonly> = { + 'host.architecture': 'host.architecture', + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.mac': 'host.mac', + 'host.name': 'host.name', + ...extendMap('host', osFieldsMap), +}; + +export const processFieldsMap: Readonly> = { + 'process.hash.md5': 'process.hash.md5', + 'process.hash.sha1': 'process.hash.sha1', + 'process.hash.sha256': 'process.hash.sha256', + 'process.pid': 'process.pid', + 'process.name': 'process.name', + 'process.ppid': 'process.ppid', + 'process.args': 'process.args', + 'process.entity_id': 'process.entity_id', + 'process.executable': 'process.executable', + 'process.title': 'process.title', + 'process.thread': 'process.thread', + 'process.working_directory': 'process.working_directory', +}; + +export const agentFieldsMap: Readonly> = { + 'agent.type': 'agent.type', +}; + +export const userFieldsMap: Readonly> = { + 'user.domain': 'user.domain', + 'user.id': 'user.id', + 'user.name': 'user.name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.full_name': 'user.full_name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.email': 'user.email', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.hash': 'user.hash', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.group': 'user.group', +}; + +export const winlogFieldsMap: Readonly> = { + 'winlog.event_id': 'winlog.event_id', +}; + +export const suricataFieldsMap: Readonly> = { + 'suricata.eve.flow_id': 'suricata.eve.flow_id', + 'suricata.eve.proto': 'suricata.eve.proto', + 'suricata.eve.alert.signature': 'suricata.eve.alert.signature', + 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id', +}; + +export const tlsFieldsMap: Readonly> = { + 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1', + 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash', + 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1', +}; + +export const urlFieldsMap: Readonly> = { + 'url.original': 'url.original', + 'url.domain': 'url.domain', + 'user.username': 'user.username', + 'user.password': 'user.password', +}; + +export const httpFieldsMap: Readonly> = { + 'http.version': 'http.version', + 'http.request': 'http.request', + 'http.request.method': 'http.request.method', + 'http.request.body.bytes': 'http.request.body.bytes', + 'http.request.body.content': 'http.request.body.content', + 'http.request.referrer': 'http.request.referrer', + 'http.response.status_code': 'http.response.status_code', + 'http.response.body': 'http.response.body', + 'http.response.body.bytes': 'http.response.body.bytes', + 'http.response.body.content': 'http.response.body.content', +}; + +export const zeekFieldsMap: Readonly> = { + 'zeek.session_id': 'zeek.session_id', + 'zeek.connection.local_resp': 'zeek.connection.local_resp', + 'zeek.connection.local_orig': 'zeek.connection.local_orig', + 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes', + 'zeek.connection.state': 'zeek.connection.state', + 'zeek.connection.history': 'zeek.connection.history', + 'zeek.notice.suppress_for': 'zeek.notice.suppress_for', + 'zeek.notice.msg': 'zeek.notice.msg', + 'zeek.notice.note': 'zeek.notice.note', + 'zeek.notice.sub': 'zeek.notice.sub', + 'zeek.notice.dst': 'zeek.notice.dst', + 'zeek.notice.dropped': 'zeek.notice.dropped', + 'zeek.notice.peer_descr': 'zeek.notice.peer_descr', + 'zeek.dns.AA': 'zeek.dns.AA', + 'zeek.dns.qclass_name': 'zeek.dns.qclass_name', + 'zeek.dns.RD': 'zeek.dns.RD', + 'zeek.dns.qtype_name': 'zeek.dns.qtype_name', + 'zeek.dns.qtype': 'zeek.dns.qtype', + 'zeek.dns.query': 'zeek.dns.query', + 'zeek.dns.trans_id': 'zeek.dns.trans_id', + 'zeek.dns.qclass': 'zeek.dns.qclass', + 'zeek.dns.RA': 'zeek.dns.RA', + 'zeek.dns.TC': 'zeek.dns.TC', + 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types', + 'zeek.http.trans_depth': 'zeek.http.trans_depth', + 'zeek.http.status_msg': 'zeek.http.status_msg', + 'zeek.http.resp_fuids': 'zeek.http.resp_fuids', + 'zeek.http.tags': 'zeek.http.tags', + 'zeek.files.session_ids': 'zeek.files.session_ids', + 'zeek.files.timedout': 'zeek.files.timedout', + 'zeek.files.local_orig': 'zeek.files.local_orig', + 'zeek.files.tx_host': 'zeek.files.tx_host', + 'zeek.files.source': 'zeek.files.source', + 'zeek.files.is_orig': 'zeek.files.is_orig', + 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes', + 'zeek.files.sha1': 'zeek.files.sha1', + 'zeek.files.duration': 'zeek.files.duration', + 'zeek.files.depth': 'zeek.files.depth', + 'zeek.files.analyzers': 'zeek.files.analyzers', + 'zeek.files.mime_type': 'zeek.files.mime_type', + 'zeek.files.rx_host': 'zeek.files.rx_host', + 'zeek.files.total_bytes': 'zeek.files.total_bytes', + 'zeek.files.fuid': 'zeek.files.fuid', + 'zeek.files.seen_bytes': 'zeek.files.seen_bytes', + 'zeek.files.missing_bytes': 'zeek.files.missing_bytes', + 'zeek.files.md5': 'zeek.files.md5', + 'zeek.ssl.cipher': 'zeek.ssl.cipher', + 'zeek.ssl.established': 'zeek.ssl.established', + 'zeek.ssl.resumed': 'zeek.ssl.resumed', + 'zeek.ssl.version': 'zeek.ssl.version', +}; + +export const sourceFieldsMap: Readonly> = { + 'source.bytes': 'source.bytes', + 'source.ip': 'source.ip', + 'source.packets': 'source.packets', + 'source.port': 'source.port', + 'source.domain': 'source.domain', + 'source.geo.continent_name': 'source.geo.continent_name', + 'source.geo.country_name': 'source.geo.country_name', + 'source.geo.country_iso_code': 'source.geo.country_iso_code', + 'source.geo.city_name': 'source.geo.city_name', + 'source.geo.region_iso_code': 'source.geo.region_iso_code', + 'source.geo.region_name': 'source.geo.region_name', +}; + +export const destinationFieldsMap: Readonly> = { + 'destination.bytes': 'destination.bytes', + 'destination.ip': 'destination.ip', + 'destination.packets': 'destination.packets', + 'destination.port': 'destination.port', + 'destination.domain': 'destination.domain', + 'destination.geo.continent_name': 'destination.geo.continent_name', + 'destination.geo.country_name': 'destination.geo.country_name', + 'destination.geo.country_iso_code': 'destination.geo.country_iso_code', + 'destination.geo.city_name': 'destination.geo.city_name', + 'destination.geo.region_iso_code': 'destination.geo.region_iso_code', + 'destination.geo.region_name': 'destination.geo.region_name', +}; + +export const networkFieldsMap: Readonly> = { + 'network.bytes': 'network.bytes', + 'network.community_id': 'network.community_id', + 'network.direction': 'network.direction', + 'network.packets': 'network.packets', + 'network.protocol': 'network.protocol', + 'network.transport': 'network.transport', +}; + +export const geoFieldsMap: Readonly> = { + 'geo.region_name': 'destination.geo.region_name', + 'geo.country_iso_code': 'destination.geo.country_iso_code', +}; + +export const dnsFieldsMap: Readonly> = { + 'dns.question.name': 'dns.question.name', + 'dns.question.type': 'dns.question.type', + 'dns.resolved_ip': 'dns.resolved_ip', + 'dns.response_code': 'dns.response_code', +}; + +export const endgameFieldsMap: Readonly> = { + 'endgame.exit_code': 'endgame.exit_code', + 'endgame.file_name': 'endgame.file_name', + 'endgame.file_path': 'endgame.file_path', + 'endgame.logon_type': 'endgame.logon_type', + 'endgame.parent_process_name': 'endgame.parent_process_name', + 'endgame.pid': 'endgame.pid', + 'endgame.process_name': 'endgame.process_name', + 'endgame.subject_domain_name': 'endgame.subject_domain_name', + 'endgame.subject_logon_id': 'endgame.subject_logon_id', + 'endgame.subject_user_name': 'endgame.subject_user_name', + 'endgame.target_domain_name': 'endgame.target_domain_name', + 'endgame.target_logon_id': 'endgame.target_logon_id', + 'endgame.target_user_name': 'endgame.target_user_name', +}; + +export const eventBaseFieldsMap: Readonly> = { + 'event.action': 'event.action', + 'event.category': 'event.category', + 'event.code': 'event.code', + 'event.created': 'event.created', + 'event.dataset': 'event.dataset', + 'event.duration': 'event.duration', + 'event.end': 'event.end', + 'event.hash': 'event.hash', + 'event.id': 'event.id', + 'event.kind': 'event.kind', + 'event.module': 'event.module', + 'event.original': 'event.original', + 'event.outcome': 'event.outcome', + 'event.risk_score': 'event.risk_score', + 'event.risk_score_norm': 'event.risk_score_norm', + 'event.severity': 'event.severity', + 'event.start': 'event.start', + 'event.timezone': 'event.timezone', + 'event.type': 'event.type', +}; + +export const systemFieldsMap: Readonly> = { + 'system.audit.package.arch': 'system.audit.package.arch', + 'system.audit.package.entity_id': 'system.audit.package.entity_id', + 'system.audit.package.name': 'system.audit.package.name', + 'system.audit.package.size': 'system.audit.package.size', + 'system.audit.package.summary': 'system.audit.package.summary', + 'system.audit.package.version': 'system.audit.package.version', + 'system.auth.ssh.signature': 'system.auth.ssh.signature', + 'system.auth.ssh.method': 'system.auth.ssh.method', +}; + +export const signalFieldsMap: Readonly> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threat': 'signal.rule.threat', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', + 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', +}; + +export const ruleFieldsMap: Readonly> = { + 'rule.reference': 'rule.reference', +}; + +export const eventFieldsMap: Readonly> = { + timestamp: '@timestamp', + '@timestamp': '@timestamp', + message: 'message', + ...{ ...agentFieldsMap }, + ...{ ...auditdMap }, + ...{ ...destinationFieldsMap }, + ...{ ...dnsFieldsMap }, + ...{ ...endgameFieldsMap }, + ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, + ...{ ...geoFieldsMap }, + ...{ ...hostFieldsMap }, + ...{ ...networkFieldsMap }, + ...{ ...ruleFieldsMap }, + ...{ ...signalFieldsMap }, + ...{ ...sourceFieldsMap }, + ...{ ...suricataFieldsMap }, + ...{ ...systemFieldsMap }, + ...{ ...tlsFieldsMap }, + ...{ ...zeekFieldsMap }, + ...{ ...httpFieldsMap }, + ...{ ...userFieldsMap }, + ...{ ...winlogFieldsMap }, + ...{ ...processFieldsMap }, +}; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts new file mode 100644 index 0000000000000..0071fe3deeb1f --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts @@ -0,0 +1,83 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { UserEcs } from '../../../../ecs/user'; +import { SourceEcs } from '../../../../ecs/source'; +import { HostEcs } from '../../../../ecs/host'; +import { + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + RequestOptionsPaginated, + StringOrNumber, + Hit, + TotalHit, +} from '../../'; + +export interface AuthenticationsStrategyResponse extends IEsSearchResponse { + edges: AuthenticationsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface AuthenticationsRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; +} + +export interface AuthenticationsEdges { + node: AuthenticationItem; + cursor: CursorType; +} + +export interface AuthenticationItem { + _id: string; + failures: number; + successes: number; + user: UserEcs; + lastSuccess?: Maybe; + lastFailure?: Maybe; +} + +export interface LastSourceHost { + timestamp?: Maybe; + source?: Maybe; + host?: Maybe; +} + +export interface AuthenticationHit extends Hit { + _source: { + '@timestamp': string; + lastSuccess?: LastSourceHost; + lastFailure?: LastSourceHost; + }; + user: string; + failures: number; + successes: number; + cursor?: string; + sort: StringOrNumber[]; +} + +export interface AuthenticationBucket { + key: { + user_uid: string; + }; + doc_count: number; + failures: { + doc_count: number; + }; + successes: { + doc_count: number; + }; + authentication: { + hits: { + total: TotalHit; + hits: ArrayLike; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index a27899e454074..dc81c0a9137f8 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -10,7 +10,8 @@ export * from './overview'; export * from './first_last_seen'; export enum HostsQueries { + authentications = 'authentications', + firstLastSeen = 'firstLastSeen', hosts = 'hosts', hostOverview = 'hostOverview', - firstLastSeen = 'firstLastSeen', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 6905f2be38966..474002c93f24f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -15,6 +15,10 @@ import { HostsRequestOptions, HostsStrategyResponse, } from './hosts'; +import { + AuthenticationsRequestOptions, + AuthenticationsStrategyResponse, +} from './hosts/authentications'; import { NetworkQueries, NetworkTlsStrategyResponse, @@ -38,7 +42,6 @@ export interface TotalValue { export interface Inspect { dsl: string[]; - response: string[]; } export interface PageInfoPaginated { @@ -96,6 +99,43 @@ export interface DocValueFields { format: string; } +export interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +export interface TotalValue { + value: number; + relation: string; +} +export interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface TotalHit { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _type: string; + _id: string; + _score: number | null; +} + +export interface Hits { + hits: { + total: T; + max_score: number | null; + hits: U[]; + }; +} + export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; filterQuery: ESQuery | string | undefined; @@ -104,6 +144,8 @@ export interface RequestBasicOptions extends IEsSearchRequest { factoryQueryType?: FactoryQueryTypes; } +/** A mapping of semantic fields to their document counterparts */ + export interface RequestOptions extends RequestBasicOptions { pagination: PaginationInput; sort: SortField; @@ -118,6 +160,8 @@ export type StrategyResponseType = T extends HostsQ ? HostsStrategyResponse : T extends HostsQueries.hostOverview ? HostOverviewStrategyResponse + : T extends HostsQueries.authentications + ? AuthenticationsStrategyResponse : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenStrategyResponse : T extends NetworkQueries.tls @@ -130,6 +174,8 @@ export type StrategyRequestType = T extends HostsQu ? HostsRequestOptions : T extends HostsQueries.hostOverview ? HostOverviewRequestOptions + : T extends HostsQueries.authentications + ? AuthenticationsRequestOptions : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenRequestOptions : T extends NetworkQueries.tls @@ -138,6 +184,8 @@ export type StrategyRequestType = T extends HostsQu ? NetworkHttpRequestOptions : never; +export type StringOrNumber = string | number; + export interface GenericBuckets { key: string; doc_count: number; diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts index 53fe185ef9a65..63c3f3ea81d98 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -6,7 +6,12 @@ import { CoreStart } from '../../../../src/core/public'; import { APP_ID } from '../common/constants'; +import { + FactoryQueryTypes, + StrategyResponseType, +} from '../common/search_strategy/security_solution'; import { SecurityPageName } from './app/types'; +import { InspectResponse } from './types'; export const manageOldSiemRoutes = async (coreStart: CoreStart) => { const { application } = coreStart; @@ -73,3 +78,11 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => { break; } }; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: response != null ? [JSON.stringify(response, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index e8bd0c58f9ace..8e2b47769adf3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -10,8 +10,8 @@ import { has } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { hostsActions, hostsModel, hostsSelectors } from '../../store'; -import { AuthenticationsEdges } from '../../../graphql/types'; +import { AuthenticationsEdges } from '../../../../common/search_strategy/security_solution/hosts/authentications'; + import { State } from '../../../common/store'; import { DragEffects, @@ -24,9 +24,11 @@ import { HostDetailsLink, IPDetailsLink } from '../../../common/components/links import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { getRowItemDraggables } from '../../../common/components/tables/helpers'; + +import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import * as i18n from './translations'; -import { getRowItemDraggables } from '../../../common/components/tables/helpers'; const tableType = hostsModel.HostsTableType.authentications; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts index 84682fd14ac6b..759b34cd258d5 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts @@ -3,11 +3,34 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; +import { AuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications'; -import { AuthenticationsData } from '../../../graphql/types'; - -export const mockData: { Authentications: AuthenticationsData } = { +export const mockData: { Authentications: AuthenticationsStrategyResponse } = { Authentications: { + rawResponse: { + aggregations: { + group_by_users: { + buckets: [ + { + key: 'SYSTEM', + doc_count: 4, + failures: { + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: null, hits: [] } }, + hits: { total: 0, max_score: null, hits: [] }, + }, + successes: { + doc_count: 4, + lastSuccess: { hits: { total: 4, max_score: null } }, + }, + }, + ], + doc_count_error_upper_bound: -1, + sum_other_doc_count: 566, + }, + }, + } as SearchResponse, totalCount: 54, edges: [ { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index efd80c5c590ed..c11de9100e71e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -4,35 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { AbortError } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { - AuthenticationsEdges, - GetAuthenticationsQuery, + Direction, + DocValueFields, + HostPolicyResponseActionStatus, + HostsQueries, PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +} from '../../../../common/search_strategy/security_solution'; import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; + AuthenticationsRequestOptions, + AuthenticationsStrategyResponse, + AuthenticationsEdges, +} from '../../../../common/search_strategy/security_solution/hosts/authentications'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; + import { hostsModel, hostsSelectors } from '../../store'; -import { authenticationsQuery } from './index.gql_query'; + +import * as i18n from './translations'; const ID = 'authenticationQuery'; export interface AuthenticationArgs { authentications: AuthenticationsEdges[]; id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loading: boolean; loadPage: (newActivePage: number) => void; @@ -41,115 +51,161 @@ export interface AuthenticationArgs { totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: AuthenticationArgs) => React.ReactNode; +interface UseAuthentications { + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; type: hostsModel.HostsType; } -export interface AuthenticationsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; -} +export const useAuthentications = ({ + docValueFields, + filterQuery, + endDate, + startDate, + type, +}: UseAuthentications): [boolean, AuthenticationArgs] => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const { activePage, limit } = useSelector( + (state: State) => getAuthenticationsSelector(state, type), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [authenticationsRequest, setAuthenticationsRequest] = useState< + AuthenticationsRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.authentications, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction: Direction.desc, + field: HostPolicyResponseActionStatus.success, + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setAuthenticationsRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [authenticationsResponse, setAuthenticationsResponse] = useState({ + authentications: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loading: true, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; - -class AuthenticationsComponentQuery extends QueryTemplatePaginated< - AuthenticationsProps, - GetAuthenticationsQuery.Query, - GetAuthenticationsQuery.Variables -> { - public render() { - const { - activePage, - children, - docValueFields, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetAuthenticationsQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - inspect: isInspected, - docValueFields: docValueFields ?? [], - }; - return ( - - query={authenticationsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const authentications = getOr([], 'source.Authentications.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const authenticationsSearch = useCallback( + (request: AuthenticationsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setAuthenticationsResponse((prevResponse) => ({ + ...prevResponse, + authentications: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + notifications.toasts.addWarning(i18n.ERROR_AUTHENTICATIONS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_AUTHENTICATIONS, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Authentications: { - ...fetchMoreResult.source.Authentications, - edges: [...fetchMoreResult.source.Authentications.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - authentications, - id, - inspect: getOr(null, 'source.Authentications.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Authentications.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Authentications.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getAuthenticationsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; + useEffect(() => { + setAuthenticationsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, startDate]); -export const AuthenticationsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(AuthenticationsComponentQuery); + useEffect(() => { + authenticationsSearch(authenticationsRequest); + }, [authenticationsRequest, authenticationsSearch]); + + return [loading, authenticationsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts new file mode 100644 index 0000000000000..95ff6363a3870 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.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 { i18n } from '@kbn/i18n'; + +export const ERROR_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.authentications.errorSearchDescription', + { + defaultMessage: `An error has occurred on authentications search`, + } +); + +export const FAIL_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.authentications.failSearchDescription', + { + defaultMessage: `Failed to run search on authentications`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 6e1ebbfd1e7bb..bc711d08065e2 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -26,6 +26,8 @@ import { ESTermQuery } from '../../../../common/typed_json'; import * as i18n from './translations'; import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; const ID = 'hostsQuery'; @@ -34,7 +36,7 @@ export interface HostsArgs { endDate: string; hosts: HostsEdges[]; id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: LoadPage; pageInfo: PageInfoPaginated; @@ -139,7 +141,7 @@ export const useAllHost = ({ setHostsResponse((prevResponse) => ({ ...prevResponse, hosts: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx index f766f068f099f..a72ef0ff1c46e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx @@ -22,12 +22,14 @@ import { import * as i18n from './translations'; import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; const ID = 'hostOverviewQuery'; export interface HostOverviewArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; hostOverview: HostItem; refetch: inputsModel.Refetch; startDate: string; @@ -97,7 +99,7 @@ export const useHostOverview = ({ setHostOverviewResponse((prevResponse) => ({ ...prevResponse, hostOverview: response.hostOverview, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), refetch: refetch.current, })); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts index ada713d135c22..c8cd36e40d6e0 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts @@ -19,3 +19,17 @@ export const FAIL_ALL_HOST = i18n.translate( defaultMessage: `Failed to run search on all hosts`, } ); + +export const ERROR_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.hostOverview.errorSearchDescription', + { + defaultMessage: `An error has occurred on host overview search`, + } +); + +export const FAIL_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.hostOverview.failSearchDescription', + { + defaultMessage: `Failed to run search on host overview`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 6c8eb9eb04941..084d4b699e8eb 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -8,7 +8,7 @@ import { getOr } from 'lodash/fp'; import React, { useEffect } from 'react'; import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { AuthenticationsQuery } from '../../containers/authentications'; +import { useAuthentications } from '../../containers/authentications'; import { HostsComponentsQueryProps } from './types'; import { hostsModel } from '../../store'; import { @@ -77,6 +77,11 @@ export const AuthenticationsQueryTabBody = ({ }; }, [deleteQuery]); + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useAuthentications({ docValueFields, endDate, filterQuery, startDate, type }); + return ( <> - - {({ - authentications, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index ae50f6919dce1..d71953eec026f 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -25,13 +25,15 @@ import { } from '../../../../common/search_strategy/security_solution'; import { AbortError } from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; +import { InspectResponse } from '../../../types'; +import { getInspectResponse } from '../../../helpers'; const ID = 'networkHttpQuery'; export interface NetworkHttpArgs { id: string; ip?: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: (newActivePage: number) => void; networkHttp: NetworkHttpEdges[]; @@ -134,7 +136,7 @@ export const useNetworkHttp = ({ setNetworkHttpResponse((prevResponse) => ({ ...prevResponse, networkHttp: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 77c6446fbb3d0..1c2604ee65552 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -23,7 +23,9 @@ import { NetworkTlsStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; import { AbortError } from '../../../../../../../src/plugins/data/common'; + import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; const ID = 'networkTlsQuery'; @@ -133,7 +135,7 @@ export const useNetworkTls = ({ setNetworkTlsResponse((prevResponse) => ({ ...prevResponse, tls: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d2b66207d8602..4fdacb2621abd 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -22,6 +22,7 @@ import { import { SecurityPluginSetup } from '../../security/public'; import { AppFrontendLibs } from './common/lib/lib'; import { ResolverPluginSetup } from './resolver/types'; +import { Inspect } from '../common/search_strategy/security_solution'; export interface SetupPlugins { home?: HomePublicPluginSetup; @@ -56,3 +57,5 @@ export interface PluginStart {} export interface AppObservableLibs extends AppFrontendLibs { kibana: CoreStart; } + +export type InspectResponse = Inspect & { response: string[] }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts new file mode 100644 index 0000000000000..4da319d5b1e42 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const toArray = (value: T | T[] | null) => + Array.isArray(value) ? value : value == null ? [] : [value]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts new file mode 100644 index 0000000000000..35e4d2cc8e1fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts @@ -0,0 +1,120 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { AuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ecs_fields'; + +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; +import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; +import { extendMap } from '../../../../../../lib/ecs_fields/extend_map'; + +import { authenticationFields } from '../helpers'; + +export const auditdFieldsMap: Readonly> = { + latest: '@timestamp', + 'lastSuccess.timestamp': 'lastSuccess.@timestamp', + 'lastFailure.timestamp': 'lastFailure.@timestamp', + ...{ ...extendMap('lastSuccess', sourceFieldsMap) }, + ...{ ...extendMap('lastSuccess', hostFieldsMap) }, + ...{ ...extendMap('lastFailure', sourceFieldsMap) }, + ...{ ...extendMap('lastFailure', hostFieldsMap) }, +}; + +export const buildQuery = ({ + filterQuery, + timerange: { from, to }, + pagination: { querySize }, + defaultIndex, + docValueFields, +}: AuthenticationsRequestOptions) => { + const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap }); + + const filter = [ + ...createQueryFilterClauses(filterQuery), + { term: { 'event.category': 'authentication' } }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { + user_count: { + cardinality: { + field: 'user.name', + }, + }, + }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...agg, + group_by_users: { + terms: { + size: querySize, + field: 'user.name', + order: [{ 'successes.doc_count': 'desc' }, { 'failures.doc_count': 'desc' }], + }, + aggs: { + failures: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + aggs: { + lastFailure: { + top_hits: { + size: 1, + _source: esFields, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + }, + successes: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + aggs: { + lastSuccess: { + top_hits: { + size: 1, + _source: esFields, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + }, + track_total_hits: false, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts new file mode 100644 index 0000000000000..722445a7275a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -0,0 +1,91 @@ +/* + * 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 { get, getOr } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { mergeFieldsWithHit } from '../../../../../utils/build_query'; +import { + AuthenticationsEdges, + AuthenticationHit, + AuthenticationBucket, +} from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { toArray } from '../../../../helpers/to_array'; +import { + FactoryQueryTypes, + StrategyResponseType, +} from '../../../../../../common/search_strategy/security_solution'; + +export const authenticationFields = [ + '_id', + 'failures', + 'successes', + 'user.name', + 'lastSuccess.timestamp', + 'lastSuccess.source.ip', + 'lastSuccess.host.id', + 'lastSuccess.host.name', + 'lastFailure.timestamp', + 'lastFailure.source.ip', + 'lastFailure.host.id', + 'lastFailure.host.name', +]; + +export const formatAuthenticationData = ( + hit: AuthenticationHit, + fieldMap: Readonly> +): AuthenticationsEdges => + authenticationFields.reduce( + (flattenedFields, fieldName) => { + if (hit.cursor) { + flattenedFields.cursor.value = hit.cursor; + } + flattenedFields.node = { + ...flattenedFields.node, + ...{ + _id: hit._id, + user: { name: [hit.user] }, + failures: hit.failures, + successes: hit.successes, + }, + }; + const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); + const fieldPath = `node.${fieldName}`; + const fieldValue = get(fieldPath, mergedResult); + + return set(fieldPath, toArray(fieldValue), mergedResult); + }, + { + node: { + failures: 0, + successes: 0, + _id: '', + user: { + name: [''], + }, + }, + cursor: { + value: '', + tiebreaker: null, + }, + } + ); + +export const getHits = (response: StrategyResponseType) => + getOr([], 'aggregations.group_by_users.buckets', response.rawResponse).map( + (bucket: AuthenticationBucket) => ({ + _id: getOr( + `${bucket.key}+${bucket.doc_count}`, + 'failures.lastFailure.hits.hits[0].id', + bucket + ), + _source: { + lastSuccess: getOr(null, 'successes.lastSuccess.hits.hits[0]._source', bucket), + lastFailure: getOr(null, 'failures.lastFailure.hits.hits[0]._source', bucket), + }, + user: bucket.key, + failures: bucket.failures.doc_count, + successes: bucket.successes.doc_count, + }) + ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx new file mode 100644 index 0000000000000..d07c239dfab86 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -0,0 +1,64 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { HostsQueries } from '../../../../../../common/search_strategy/security_solution'; +import { + AuthenticationsEdges, + AuthenticationsRequestOptions, + AuthenticationsStrategyResponse, + AuthenticationHit, +} from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/query.dsl'; +import { formatAuthenticationData, getHits } from './helpers'; + +export const authentications: SecuritySolutionFactory = { + buildDsl: (options: AuthenticationsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildAuthenticationQuery(options); + }, + parse: async ( + options: AuthenticationsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); + + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const hits: AuthenticationHit[] = getHits(response); + const authenticationEdges: AuthenticationsEdges[] = hits.map((hit) => + formatAuthenticationData(hit, auditdFieldsMap) + ); + + const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildAuthenticationQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts index a7ec822839d21..48e210d822918 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts @@ -13,6 +13,8 @@ import { hostFieldsMap } from '../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../lib/hosts/types'; +import { toArray } from '../../../helpers/to_array'; + const hostsFields = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => @@ -23,11 +25,7 @@ export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set( - `node.${fieldName}`, - Array.isArray(fieldValue) ? fieldValue : [fieldValue], - flattenedFields - ); + return set(`node.${fieldName}`, toArray(fieldValue), flattenedFields); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 34676fc1932fe..ddd2a458b3b8c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -11,9 +11,11 @@ import { SecuritySolutionFactory } from '../types'; import { allHosts } from './all'; import { overviewHost } from './overview'; import { firstLastSeenHost } from './last_first_seen'; +import { authentications } from './authentications'; export const hostsFactory: Record> = { [HostsQueries.hosts]: allHosts, [HostsQueries.hostOverview]: overviewHost, [HostsQueries.firstLastSeen]: firstLastSeenHost, + [HostsQueries.authentications]: authentications, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts index b6c26cd533de2..c0205ccce63cc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts @@ -40,7 +40,6 @@ export const networkHttp: SecuritySolutionFactory = { const edges = networkHttpEdges.splice(cursorStart, querySize - cursorStart); const inspect = { dsl: [inspectStringifyObject(buildHttpQuery(options))], - response: [inspectStringifyObject(response)], }; const showMorePagesIndicator = totalCount > fakeTotalCount;