diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8f2c4578d055f..ad877ec69945d 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -64,20 +64,21 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator'; export const INDICATOR_DESTINATION_PATH = 'threat.indicator'; export enum SecurityPageName { - overview = 'overview', - detections = 'detections', + administration = 'administration', alerts = 'alerts', - rules = 'rules', + case = 'case', + detections = 'detections', + endpoints = 'endpoints', + eventFilters = 'event_filters', exceptions = 'exceptions', hosts = 'hosts', network = 'network', - timelines = 'timelines', - case = 'case', - administration = 'administration', - endpoints = 'endpoints', + overview = 'overview', policies = 'policies', + rules = 'rules', + timelines = 'timelines', trustedApps = 'trusted_apps', - eventFilters = 'event_filters', + ueba = 'ueba', } export const TIMELINES_PATH = '/timelines'; @@ -88,6 +89,7 @@ export const ALERTS_PATH = '/alerts'; export const RULES_PATH = '/rules'; export const EXCEPTIONS_PATH = '/exceptions'; export const HOSTS_PATH = '/hosts'; +export const UEBA_PATH = '/ueba'; export const NETWORK_PATH = '/network'; export const MANAGEMENT_PATH = '/administration'; export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`; @@ -102,6 +104,7 @@ export const APP_RULES_PATH = `${APP_PATH}${RULES_PATH}`; export const APP_EXCEPTIONS_PATH = `${APP_PATH}${EXCEPTIONS_PATH}`; export const APP_HOSTS_PATH = `${APP_PATH}${HOSTS_PATH}`; +export const APP_UEBA_PATH = `${APP_PATH}${UEBA_PATH}`; export const APP_NETWORK_PATH = `${APP_PATH}${NETWORK_PATH}`; export const APP_TIMELINES_PATH = `${APP_PATH}${TIMELINES_PATH}`; export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}`; @@ -121,6 +124,11 @@ export const DEFAULT_INDEX_PATTERN = [ 'winlogbeat-*', ]; +export const DEFAULT_INDEX_PATTERN_EXPERIMENTAL = [ + // TODO: Steph/ueba TEMP for testing UEBA data + 'ml_host_risk_score_*', +]; + /** This Kibana Advanced Setting enables the `Security news` feed widget */ export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed'; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index a9a81aa285af7..6d4a2b78840ea 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -11,11 +11,12 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; * A list of allowed values that can be used in `xpack.securitySolution.enableExperimental`. * This object is then used to validate and parse the value entered. */ -const allowedExperimentalValues = Object.freeze({ - trustedAppsByPolicyEnabled: false, +export const allowedExperimentalValues = Object.freeze({ metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: false, + trustedAppsByPolicyEnabled: false, + uebaEnabled: false, }); type ExperimentalConfigKeys = Array; 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 06d4a16699b8f..208579ffacabe 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 @@ -71,14 +71,27 @@ import { CtiEventEnrichmentStrategyResponse, CtiQueries, } from './cti'; +import { + HostRulesRequestOptions, + HostRulesStrategyResponse, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, + UebaQueries, + UserRulesRequestOptions, + UserRulesStrategyResponse, +} from './ueba'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; +export * from './ueba'; export type FactoryQueryTypes = | HostsQueries | HostsKpiQueries + | UebaQueries | NetworkQueries | NetworkKpiQueries | CtiQueries @@ -109,6 +122,14 @@ export type StrategyResponseType = T extends HostsQ ? HostsStrategyResponse : T extends HostsQueries.details ? HostDetailsStrategyResponse + : T extends UebaQueries.riskScore + ? RiskScoreStrategyResponse + : T extends UebaQueries.hostRules + ? HostRulesStrategyResponse + : T extends UebaQueries.userRules + ? UserRulesStrategyResponse + : T extends UebaQueries.hostTactics + ? HostTacticsStrategyResponse : T extends HostsQueries.overview ? HostsOverviewStrategyResponse : T extends HostsQueries.authentications @@ -199,6 +220,14 @@ export type StrategyRequestType = T extends HostsQu ? NetworkKpiUniqueFlowsRequestOptions : T extends NetworkKpiQueries.uniquePrivateIps ? NetworkKpiUniquePrivateIpsRequestOptions + : T extends UebaQueries.riskScore + ? RiskScoreRequestOptions + : T extends UebaQueries.hostRules + ? HostRulesRequestOptions + : T extends UebaQueries.userRules + ? UserRulesRequestOptions + : T extends UebaQueries.hostTactics + ? HostTacticsRequestOptions : T extends typeof MatrixHistogramQuery ? MatrixHistogramRequestOptions : T extends CtiQueries.eventEnrichment diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts new file mode 100644 index 0000000000000..f7406e32d1869 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Maybe } from '../../../common'; + +export enum RiskScoreFields { + hostName = 'host_name', + riskKeyword = 'risk_keyword', + riskScore = 'risk_score', +} +export interface RiskScoreItem { + _id?: Maybe; + [RiskScoreFields.hostName]: Maybe; + [RiskScoreFields.riskKeyword]: Maybe; + [RiskScoreFields.riskScore]: Maybe; +} +export enum HostRulesFields { + hits = 'hits', + riskScore = 'risk_score', + ruleName = 'rule_name', + ruleType = 'rule_type', +} +export interface HostRulesItem { + _id?: Maybe; + [HostRulesFields.hits]: Maybe; + [HostRulesFields.riskScore]: Maybe; + [HostRulesFields.ruleName]: Maybe; + [HostRulesFields.ruleType]: Maybe; +} +export enum UserRulesFields { + userName = 'user_name', + riskScore = 'risk_score', + rules = 'rules', + ruleCount = 'rule_count', +} +export enum HostTacticsFields { + hits = 'hits', + riskScore = 'risk_score', + tactic = 'tactic', + technique = 'technique', +} +export interface HostTacticsItem { + _id?: Maybe; + [HostTacticsFields.hits]: Maybe; + [HostTacticsFields.riskScore]: Maybe; + [HostTacticsFields.tactic]: Maybe; + [HostTacticsFields.technique]: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts new file mode 100644 index 0000000000000..cb6469c6209a6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostRulesItem, HostRulesFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface HostRulesHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + rule_type: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; + rule_count: { + value: number; + }; +} + +export interface HostRulesEdges { + node: HostRulesItem; + cursor: CursorType; +} + +export interface HostRulesStrategyResponse extends IEsSearchResponse { + edges: HostRulesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostRulesRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type HostRulesSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts new file mode 100644 index 0000000000000..c55058dc6be04 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostTacticsItem, HostTacticsFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +export interface HostTechniqueHit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; +} +export interface HostTacticsHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + technique: { + buckets?: HostTechniqueHit[]; + }; + tactic_count: { + value: number; + }; +} + +export interface HostTacticsEdges { + node: HostTacticsItem; + cursor: CursorType; +} + +export interface HostTacticsStrategyResponse extends IEsSearchResponse { + edges: HostTacticsEdges[]; + techniqueCount: number; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostTacticsRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type HostTacticsSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts new file mode 100644 index 0000000000000..1d166e36f6973 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './common'; +export * from './host_rules'; +export * from './host_tactics'; +export * from './risk_score'; +export * from './user_rules'; + +export enum UebaQueries { + hostRules = 'hostRules', + hostTactics = 'hostTactics', + riskScore = 'riskScore', + userRules = 'userRules', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts new file mode 100644 index 0000000000000..14c1533755056 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { RiskScoreItem, RiskScoreFields } from '../common'; +import { CursorType, Hit, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface RiskScoreHit extends Hit { + _source: { + '@timestamp': string; + }; + key: string; + doc_count: number; + risk_score: { + value?: number; + }; + risk_keyword: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; +} + +export interface RiskScoreEdges { + node: RiskScoreItem; + cursor: CursorType; +} + +export interface RiskScoreStrategyResponse extends IEsSearchResponse { + edges: RiskScoreEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface RiskScoreRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; +} + +export type RiskScoreSortField = SortField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts new file mode 100644 index 0000000000000..c7302c10fab3b --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostRulesFields, UserRulesFields } from '../common'; +import { Hit, Inspect, Maybe, PageInfoPaginated, SearchHit, SortField } from '../../../common'; +import { HostRulesEdges, RequestOptionsPaginated } from '../..'; + +export interface RuleNameHit extends Hit { + key: string; + doc_count: number; + risk_score: { + value: number; + }; + rule_type: { + buckets?: Array<{ + key: string; + doc_count: number; + }>; + }; +} +export interface UserRulesHit extends Hit { + _source: { + '@timestamp': string; + }; + key: string; + doc_count: number; + risk_score: { + value: number; + }; + rule_count: { + value: number; + }; + rule_name: { + buckets?: RuleNameHit[]; + }; +} + +export interface UserRulesByUser { + _id?: Maybe; + [UserRulesFields.userName]: string; + [UserRulesFields.riskScore]: number; + [UserRulesFields.ruleCount]: number; + [UserRulesFields.rules]: HostRulesEdges[]; +} + +export interface UserRulesStrategyUserResponse { + [UserRulesFields.userName]: string; + [UserRulesFields.riskScore]: number; + edges: HostRulesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; +} + +export interface UserRulesStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + data: UserRulesStrategyUserResponse[]; +} + +export interface UserRulesRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; + hostName: string; +} + +export type UserRulesSortField = SortField; + +export interface UsersRulesHit extends SearchHit { + aggregations: { + user_data: { + buckets: UserRulesHit[]; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 05cf99195774b..e7c6464bc1546 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -308,6 +308,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes @@ -320,6 +321,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.uebaPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), ]); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index f125218b68c09..59af6737e495f 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -7,13 +7,14 @@ import { getDeepLinks } from '.'; import { Capabilities } from '../../../../../../src/core/public'; import { SecurityPageName } from '../types'; +import { mockGlobalState } from '../../common/mock'; describe('public search functions', () => { it('returns a subset of links for basic license, full set for platinum', () => { const basicLicense = 'basic'; const platinumLicense = 'platinum'; - const basicLinks = getDeepLinks(basicLicense); - const platinumLinks = getDeepLinks(platinumLicense); + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense); + const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense); basicLinks.forEach((basicLink, index) => { const platinumLink = platinumLinks[index]; @@ -26,7 +27,7 @@ describe('public search functions', () => { it('returns case links for basic license with only read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: false }, } as unknown) as Capabilities); @@ -35,7 +36,7 @@ describe('public search functions', () => { it('returns case links with NO deepLinks for basic license with only read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: false }, } as unknown) as Capabilities); @@ -46,7 +47,7 @@ describe('public search functions', () => { it('returns case links with deepLinks for basic license with crud_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: true, crud_cases: true }, } as unknown) as Capabilities); @@ -57,7 +58,7 @@ describe('public search functions', () => { it('returns NO case links for basic license with NO read_cases capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, ({ + const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ siem: { read_cases: false, crud_cases: false }, } as unknown) as Capabilities); @@ -66,17 +67,38 @@ describe('public search functions', () => { it('returns case links for basic license with undefined capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, undefined); + const basicLinks = getDeepLinks( + mockGlobalState.app.enableExperimental, + basicLicense, + undefined + ); expect(basicLinks.some((l) => l.id === SecurityPageName.case)).toBeTruthy(); }); it('returns case deepLinks for basic license with undefined capabilities', () => { const basicLicense = 'basic'; - const basicLinks = getDeepLinks(basicLicense, undefined); + const basicLinks = getDeepLinks( + mockGlobalState.app.enableExperimental, + basicLicense, + undefined + ); expect( (basicLinks.find((l) => l.id === SecurityPageName.case)?.deepLinks?.length ?? 0) > 0 ).toBeTruthy(); }); + + it('returns NO ueba link when enableExperimental.uebaEnabled === false', () => { + const deepLinks = getDeepLinks(mockGlobalState.app.enableExperimental); + expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeFalsy(); + }); + + it('returns ueba link when enableExperimental.uebaEnabled === true', () => { + const deepLinks = getDeepLinks({ + ...mockGlobalState.app.enableExperimental, + uebaEnabled: true, + }); + expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 01e192d92f4f3..c679828a1c494 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -27,6 +27,7 @@ import { TIMELINES, CASE, MANAGE, + UEBA, } from '../translations'; import { OVERVIEW_PATH, @@ -40,7 +41,9 @@ import { ENDPOINTS_PATH, TRUSTED_APPS_PATH, EVENT_FILTERS_PATH, + UEBA_PATH, } from '../../../common/constants'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; export const topDeepLinks: AppDeepLink[] = [ { @@ -90,6 +93,18 @@ export const topDeepLinks: AppDeepLink[] = [ ], order: 9003, }, + { + id: SecurityPageName.ueba, + title: UEBA, + path: UEBA_PATH, + navLinkStatus: AppNavLinkStatus.visible, + keywords: [ + i18n.translate('xpack.securitySolution.search.ueba', { + defaultMessage: 'Users & Entities', + }), + ], + order: 9004, + }, { id: SecurityPageName.timelines, title: TIMELINES, @@ -100,7 +115,7 @@ export const topDeepLinks: AppDeepLink[] = [ defaultMessage: 'Timelines', }), ], - order: 9004, + order: 9005, }, { id: SecurityPageName.case, @@ -112,7 +127,7 @@ export const topDeepLinks: AppDeepLink[] = [ defaultMessage: 'Cases', }), ], - order: 9005, + order: 9006, }, { id: SecurityPageName.administration, @@ -254,6 +269,9 @@ const nestedDeepLinks: SecurityDeepLinks = { }, ], }, + [SecurityPageName.ueba]: { + base: [], + }, [SecurityPageName.timelines]: { base: [ { @@ -316,18 +334,22 @@ const nestedDeepLinks: SecurityDeepLinks = { /** * A function that generates the plugin deepLinks + * @param enableExperimental ExperimentalFeatures arg * @param licenseType optional string for license level, if not provided basic is assumed. + * @param capabilities optional arg for app start capabilities */ export function getDeepLinks( + enableExperimental: ExperimentalFeatures, licenseType?: LicenseType, capabilities?: ApplicationStart['capabilities'] ): AppDeepLink[] { return topDeepLinks .filter( (deepLink) => - deepLink.id !== SecurityPageName.case || - capabilities == null || - (deepLink.id === SecurityPageName.case && capabilities.siem.read_cases === true) + (deepLink.id !== SecurityPageName.case && deepLink.id !== SecurityPageName.ueba) || // is not cases or ueba + (deepLink.id === SecurityPageName.case && + (capabilities == null || capabilities.siem.read_cases === true)) || // is cases with at least read only caps + (deepLink.id === SecurityPageName.ueba && enableExperimental.uebaEnabled) // is ueba with ueba feature flag enabled ) .map((deepLink) => { const deepLinkId = deepLink.id as SecurityDeepLinkName; @@ -370,11 +392,13 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { export function updateGlobalNavigation({ capabilities, updater$, + enableExperimental, }: { capabilities: ApplicationStart['capabilities']; updater$: Subject; + enableExperimental: ExperimentalFeatures; }) { - const deepLinks = getDeepLinks(undefined, capabilities); + const deepLinks = getDeepLinks(enableExperimental, undefined, capabilities); const updatedDeepLinks = deepLinks.map((link) => { switch (link.id) { case SecurityPageName.case: diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index d6f8516d43a72..686dafca76d99 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -24,6 +24,7 @@ import { APP_ENDPOINTS_PATH, APP_TRUSTED_APPS_PATH, APP_EVENT_FILTERS_PATH, + APP_UEBA_PATH, SecurityPageName, } from '../../../common/constants'; @@ -70,6 +71,13 @@ export const navTabs: SecurityNav = { disabled: false, urlKey: 'network', }, + [SecurityPageName.ueba]: { + id: SecurityPageName.ueba, + name: i18n.UEBA, + href: APP_UEBA_PATH, + disabled: false, + urlKey: 'ueba', + }, [SecurityPageName.timelines]: { id: SecurityPageName.timelines, name: i18n.TIMELINES, diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 81437ec9ec6f6..e880da57cf374 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Redirect, Route, Switch } from 'react-router-dom'; import { OVERVIEW_PATH } from '../../common/constants'; -import { NotFoundPage } from '../app/404'; +import { NotFoundPage } from './404'; import { SecurityApp } from './app'; import { RenderAppProps } from './types'; @@ -43,6 +43,8 @@ export const renderApp = ({ ...subPlugins.exceptions.routes, ...subPlugins.hosts.routes, ...subPlugins.network.routes, + // will be undefined if enabledExperimental.uebaEnabled === false + ...(subPlugins.ueba != null ? subPlugins.ueba.routes : []), ...subPlugins.timelines.routes, ...subPlugins.cases.routes, ...subPlugins.management.routes, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 027789713a2ae..c3cf11f35211e 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -19,6 +19,10 @@ export const NETWORK = i18n.translate('xpack.securitySolution.navigation.network defaultMessage: 'Network', }); +export const UEBA = i18n.translate('xpack.securitySolution.navigation.ueba', { + defaultMessage: 'Users & Entities', +}); + export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { defaultMessage: 'Rules', }); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 8056c4092091c..490ff8936c18c 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -54,19 +54,21 @@ export interface SecuritySubPlugin { export type SecuritySubPluginKeyStore = | 'hosts' | 'network' + | 'ueba' | 'timeline' | 'hostList' | 'alertList' | 'management'; export type SecurityDeepLinkName = - | SecurityPageName.overview + | SecurityPageName.administration + | SecurityPageName.case | SecurityPageName.detections | SecurityPageName.hosts | SecurityPageName.network + | SecurityPageName.overview | SecurityPageName.timelines - | SecurityPageName.case - | SecurityPageName.administration; + | SecurityPageName.ueba; interface SecurityDeepLink { base: AppDeepLink[]; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index dea19e1366875..46d05d9712227 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -77,6 +77,7 @@ export interface HeaderPageProps extends HeaderProps { children?: React.ReactNode; draggableArguments?: DraggableArguments; hideSourcerer?: boolean; + sourcererScope?: SourcererScopeName; subtitle?: SubtitleProps['items']; subtitle2?: SubtitleProps['items']; title: TitleProp; @@ -115,6 +116,7 @@ const HeaderPageComponent: React.FC = ({ draggableArguments, hideSourcerer = false, isLoading, + sourcererScope = SourcererScopeName.default, subtitle, subtitle2, title, @@ -145,7 +147,7 @@ const HeaderPageComponent: React.FC = ({ {children} )} - {!hideSourcerer && } + {!hideSourcerer && } {/* Manually add a 'padding-bottom' to header */} diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx new file mode 100644 index 0000000000000..614ddf698d6b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UebaTableType } from '../../../ueba/store/model'; +import { UEBA_PATH } from '../../../../common/constants'; +import { appendSearch } from './helpers'; + +export const getUebaUrl = (search?: string) => `${UEBA_PATH}${appendSearch(search)}`; + +export const getTabsOnUebaUrl = (tabName: UebaTableType, search?: string) => + `/${tabName}${appendSearch(search)}`; + +export const getUebaDetailsUrl = (detailName: string, search?: string) => + `/${detailName}${appendSearch(search)}`; + +export const getTabsOnUebaDetailsUrl = ( + detailName: string, + tabName: UebaTableType, + search?: string +) => `/${detailName}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 0b6b77aab00e4..cc0fdb3923dce 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -42,6 +42,7 @@ import { isUrlInvalid } from '../../utils/validators'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; +import { getUebaDetailsUrl } from '../link_to/redirect_to_ueba'; export const DEFAULT_NUMBER_OF_LINK = 5; @@ -61,6 +62,45 @@ export const PortContainer = styled.div` `; // Internal Links +const UebaDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + hostName: string; + isButton?: boolean; +}> = ({ children, hostName, isButton }) => { + const { formatUrl, search } = useFormatUrl(SecurityPageName.ueba); + const { navigateToApp } = useKibana().services.application; + const goToUebaDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.ueba, + path: getUebaDetailsUrl(encodeURIComponent(hostName), search), + }); + }, + [hostName, navigateToApp, search] + ); + + return isButton ? ( + + {children ? children : hostName} + + ) : ( + + {children ? children : hostName} + + ); +}; + +export const UebaDetailsLink = React.memo(UebaDetailsLinkComponent); + const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 4ad26533cb58c..aae97d90cb4b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -15,6 +15,7 @@ import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/p import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; +import { getBreadcrumbs as getUebaBreadcrumbs } from '../../../../ueba/pages/details/utils'; import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; import { SecurityPageName } from '../../../../app/types'; import { @@ -23,6 +24,7 @@ import { NetworkRouteSpyState, TimelineRouteSpyState, AdministrationRouteSpyState, + UebaRouteSpyState, } from '../../../utils/route/types'; import { getAppOverviewUrl } from '../../link_to'; @@ -60,6 +62,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SecurityPageName.hosts; +const isUebaRoutes = (spyState: RouteSpyState): spyState is UebaRouteSpyState => + spyState != null && spyState.pageName === SecurityPageName.ueba; + const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => spyState != null && spyState.pageName === SecurityPageName.timelines; @@ -124,6 +129,25 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isUebaRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'ueba', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + siemRootBreadcrumb, + ...getUebaBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ), + getUrlForApp + ), + ]; + } if (isRulesRoutes(spyState) && object.navTabs) { const tempNav: SearchNavTab = { urlKey: SecurityPageName.rules, isDetailPage: false }; let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 2ca0d878078aa..4d9a8a704dde5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import deepEqual from 'fast-deep-equal'; -import { useNavigation } from '../../../lib/kibana/hooks'; +import { useNavigation } from '../../../lib/kibana'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry'; import { TabNavigationProps, TabNavigationItemProps } from './types'; @@ -84,7 +84,6 @@ export const TabNavigationComponent: React.FC = ({ () => Object.values(navTabs).map((tab) => { const isSelected = selectedTabId === tab.id; - return ( ; -} export interface TabNavigationComponentProps { pageName: string; tabName: SiemRouteType | undefined; @@ -43,22 +39,30 @@ export interface NavTab { urlKey?: UrlStateType; pageId?: SecurityPageName; } + export type SecurityNavKey = - | SecurityPageName.overview + | SecurityPageName.administration + | SecurityPageName.alerts + | SecurityPageName.case + | SecurityPageName.endpoints + | SecurityPageName.eventFilters + | SecurityPageName.exceptions | SecurityPageName.hosts | SecurityPageName.network - | SecurityPageName.alerts + | SecurityPageName.overview | SecurityPageName.rules - | SecurityPageName.exceptions | SecurityPageName.timelines - | SecurityPageName.case - | SecurityPageName.administration - | SecurityPageName.endpoints | SecurityPageName.trustedApps - | SecurityPageName.eventFilters; + | SecurityPageName.ueba; export type SecurityNav = Record; +export type GenericNavRecord = Record; + +export interface SecuritySolutionTabNavigationProps { + display?: 'default' | 'condensed'; + navTabs: GenericNavRecord; +} export type GetUrlForApp = ( appId: string, options?: { deepLinkId?: string; path?: string; absolute?: boolean } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index af88aacb7602a..4bd5a43684792 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -16,10 +16,12 @@ import { TimelineTabs } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { UrlInputsModel } from '../../../store/inputs/model'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); +jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); describe('useSecuritySolutionNavigation', () => { @@ -70,6 +72,7 @@ describe('useSecuritySolutionNavigation', () => { ]; beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); (useKibana as jest.Mock).mockReturnValue({ @@ -231,6 +234,17 @@ describe('useSecuritySolutionNavigation', () => { `); }); + // TODO: Steph/ueba remove when no longer experimental + it('should include ueba when feature flag is on', async () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => + useSecuritySolutionNavigation() + ); + + // @ts-ignore possibly undefined, but if undefined we want this test to fail + expect(result.current.items[2].items[2].id).toEqual(SecurityPageName.ueba); + }); + describe('Permission gated routes', () => { describe('cases', () => { it('should display the cases navigation item when the user has read permissions', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index 39c6885e8dff5..5165a903bbde1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -13,6 +13,8 @@ import { makeMapStateToProps } from '../../url_state/helpers'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { navTabs } from '../../../../app/home/home_navigations'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; +import { GenericNavRecord } from '../types'; /** * @description - This hook provides the structure necessary by the KibanaPageTemplate for rendering the primary security_solution side navigation. @@ -29,6 +31,12 @@ export const useSecuritySolutionNavigation = () => { const { detailName, flowTarget, pageName, pathName, search, state, tabName } = routeProps; + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); + let enabledNavTabs: GenericNavRecord = (navTabs as unknown) as GenericNavRecord; + if (!uebaEnabled) { + const { ueba, ...rest } = enabledNavTabs; + enabledNavTabs = rest; + } useEffect(() => { if (pathName || pageName) { setBreadcrumbs( @@ -36,7 +44,7 @@ export const useSecuritySolutionNavigation = () => { detailName, filters: urlState.filters, flowTarget, - navTabs, + navTabs: enabledNavTabs, pageName, pathName, query: urlState.query, @@ -65,12 +73,13 @@ export const useSecuritySolutionNavigation = () => { tabName, getUrlForApp, navigateToUrl, + enabledNavTabs, ]); return usePrimaryNavigation({ query: urlState.query, filters: urlState.filters, - navTabs, + navTabs: enabledNavTabs, pageName, sourcerer: urlState.sourcerer, savedQuery: urlState.savedQuery, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index fffe59fceff41..feeeacf6124e8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -20,7 +20,6 @@ export const usePrimaryNavigationItems = ({ ...urlStateProps }: PrimaryNavigationItemsProps): Array> => { const { navigateTo, getAppUrl } = useNavigation(); - const getSideNav = useCallback( (tab: NavTab) => { const { id, name, disabled } = tab; @@ -62,7 +61,6 @@ export const usePrimaryNavigationItems = ({ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - return useMemo( () => [ { @@ -76,7 +74,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.explore, - items: [navTabs.hosts, navTabs.network], + items: [navTabs.hosts, navTabs.network, ...(navTabs.ueba != null ? [navTabs.ueba] : [])], }, { ...securityNavGroup.investigate, diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 3d0be80e3d58c..f5828c9f65db9 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -46,6 +46,9 @@ import { useStateToaster } from '../toasters'; import * as i18n from './translations'; import { Panel } from '../panel'; import { InspectButtonContainer } from '../inspect'; +import { RiskScoreColumns } from '../../../ueba/components/risk_score_table'; +import { HostRulesColumns } from '../../../ueba/components/host_rules_table'; +import { HostTacticsColumns } from '../../../ueba/components/host_tactics_table'; const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; @@ -74,6 +77,8 @@ declare type HostsTableColumnsTest = [ declare type BasicTableColumns = | AuthTableColumns + | HostRulesColumns + | HostTacticsColumns | HostsTableColumns | HostsTableColumnsTest | NetworkDnsColumns @@ -82,6 +87,8 @@ declare type BasicTableColumns = | NetworkTopCountriesColumnsNetworkDetails | NetworkTopNFlowColumns | NetworkTopNFlowColumnsNetworkDetails + | NetworkHttpColumns + | RiskScoreColumns | TlsColumns | UncommonProcessTableColumns | UsersColumns; @@ -97,7 +104,8 @@ export interface BasicTableProps { headerSupplement?: React.ReactElement; headerTitle: string | React.ReactElement; headerTooltip?: string; - headerUnit: string | React.ReactElement; + headerUnit?: string | React.ReactElement; + headerSubtitle?: string | React.ReactElement; id?: string; itemsPerRow?: ItemsPerRow[]; isInspect?: boolean; @@ -136,6 +144,7 @@ const PaginatedTableComponent: FC = ({ headerTitle, headerTooltip, headerUnit, + headerSubtitle, id, isInspect, itemsPerRow, @@ -248,8 +257,12 @@ const PaginatedTableComponent: FC = ({ = 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` + !loadingInitial && headerSubtitle + ? `${i18n.SHOWING}: ${headerSubtitle}` + : headerUnit && + `${i18n.SHOWING}: ${ + headerCount >= 0 ? headerCount.toLocaleString() : 0 + } ${headerUnit}` } title={headerTitle} tooltip={headerTooltip} diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 6107b61638888..edf09a52006fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -26,12 +26,13 @@ export enum CONSTANTS { } export type UrlStateType = - | 'case' + | 'administration' | 'alerts' - | 'rules' + | 'case' | 'exceptions' | 'host' | 'network' | 'overview' + | 'rules' | 'timeline' - | 'administration'; + | 'ueba'; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 63511c54d28db..e6f79d3d24ae0 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -19,7 +19,7 @@ import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { RouteSpyState } from '../../utils/route/types'; import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; -import { NavTab } from '../navigation/types'; +import { SecurityNav } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; import { SourcererScopePatterns } from '../../store/sourcerer/model'; @@ -66,6 +66,14 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], + ueba: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.sourcerer, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], administration: [], network: [ CONSTANTS.appQuery, @@ -124,7 +132,7 @@ export interface UrlState { export type KeyUrlState = keyof UrlState; export interface UrlStateProps { - navTabs: Record; + navTabs: SecurityNav; indexPattern?: IIndexPattern; mapToUrlState?: (value: string) => UrlState; onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 002c40fc9d428..d804f350a7f79 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -14,8 +14,8 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIndexFields } from '../source'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; -import { ALERTS_PATH, RULES_PATH } from '../../../../common/constants'; -import { TimelineId } from '../../../../common/types/timeline'; +import { ALERTS_PATH, RULES_PATH, UEBA_PATH } from '../../../../common/constants'; +import { TimelineId } from '../../../../common'; import { useDeepEqualSelector } from '../../hooks/use_selector'; export const useInitSourcerer = ( @@ -57,8 +57,7 @@ export const useInitSourcerer = ( !loadingSignalIndex && signalIndexName != null && signalIndexNameSelector == null && - (activeTimeline == null || - (activeTimeline != null && activeTimeline.savedObjectId == null)) && + (activeTimeline == null || activeTimeline.savedObjectId == null) && initialTimelineSourcerer.current ) { initialTimelineSourcerer.current = false; @@ -70,8 +69,7 @@ export const useInitSourcerer = ( ); } else if ( signalIndexNameSelector != null && - (activeTimeline == null || - (activeTimeline != null && activeTimeline.savedObjectId == null)) && + (activeTimeline == null || activeTimeline.savedObjectId == null) && initialTimelineSourcerer.current ) { initialTimelineSourcerer.current = false; @@ -124,15 +122,14 @@ export const useInitSourcerer = ( export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => { const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const SourcererScope = useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); - return SourcererScope; + return useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); }; export const getScopeFromPath = ( pathname: string ): SourcererScopeName.default | SourcererScopeName.detections => { return matchPath(pathname, { - path: [ALERTS_PATH, `${RULES_PATH}/id/:id`], + path: [ALERTS_PATH, `${RULES_PATH}/id/:id`, `${UEBA_PATH}/:id`], strict: false, }) == null ? SourcererScopeName.default diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts index 247b7624914cf..9a6b8c54f2bc6 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts @@ -14,8 +14,8 @@ import { const allowedExperimentalValues = getExperimentalAllowedValues(); -export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { - return useSelector(({ app: { enableExperimental } }: State) => { +export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => + useSelector(({ app: { enableExperimental } }: State) => { if (!enableExperimental || !(feature in enableExperimental)) { throw new Error( `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join( @@ -25,4 +25,3 @@ export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatu } return enableExperimental[feature]; }); -}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 44a100e27e95b..f8a77d97b8700 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -172,7 +172,7 @@ const createCoreStartMock = ( ): ReturnType => { const coreStart = coreMock.createStart({ basePath: '/mock' }); - const deepLinkPaths = getDeepLinkPaths(getDeepLinks()); + const deepLinkPaths = getDeepLinkPaths(getDeepLinks(mockGlobalState.app.enableExperimental)); // Mock the certain APP Ids returned by `application.getUrlForApp()` coreStart.application.getUrlForApp.mockImplementation((appId, { deepLinkId, path } = {}) => { diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index ffbfd1a5123ad..8130a7058700d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -13,6 +13,9 @@ import { NetworkTopTablesFields, NetworkTlsFields, NetworkUsersFields, + RiskScoreFields, + HostRulesFields, + HostTacticsFields, } from '../../../common/search_strategy'; import { State } from '../store'; @@ -25,12 +28,14 @@ import { DEFAULT_INDEX_PATTERN, } from '../../../common/constants'; import { networkModel } from '../../network/store'; +import { uebaModel } from '../../ueba/store'; import { TimelineType, TimelineStatus, TimelineTabs } from '../../../common/types/timeline'; import { mockManagementState } from '../../management/store/reducer'; import { ManagementState } from '../../management/types'; import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model'; import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock'; import { mockIndexPattern } from './index_pattern'; +import { allowedExperimentalValues } from '../../../common/experimental_features'; export const mockGlobalState: State = { app: { @@ -39,12 +44,7 @@ export const mockGlobalState: State = { { id: 'error-id-1', title: 'title-1', message: ['error-message-1'] }, { id: 'error-id-2', title: 'title-2', message: ['error-message-2'] }, ], - enableExperimental: { - trustedAppsByPolicyEnabled: false, - metricsEntitiesEnabled: false, - ruleRegistryEnabled: false, - tGridEnabled: false, - }, + enableExperimental: allowedExperimentalValues, }, hosts: { page: { @@ -164,6 +164,36 @@ export const mockGlobalState: State = { }, }, }, + ueba: { + page: { + queries: { + [uebaModel.UebaTableType.riskScore]: { + activePage: 0, + limit: 10, + sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, + }, + }, + }, + details: { + queries: { + [uebaModel.UebaTableType.hostRules]: { + activePage: 0, + limit: 10, + sort: { field: HostRulesFields.riskScore, direction: Direction.desc }, + }, + [uebaModel.UebaTableType.hostTactics]: { + activePage: 0, + limit: 10, + sort: { field: HostTacticsFields.riskScore, direction: Direction.desc }, + }, + [uebaModel.UebaTableType.userRules]: { + activePage: 0, + limit: 10, + sort: { field: HostRulesFields.riskScore, direction: Direction.desc }, + }, + }, + }, + }, inputs: { global: { timerange: { diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index e0f8e651a5821..0d9e2f4f367ec 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -12,6 +12,7 @@ import { tGridReducer } from '../../../../timelines/public'; import { hostsReducer } from '../../hosts/store'; import { networkReducer } from '../../network/store'; +import { uebaReducer } from '../../ueba/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; import { managementReducer } from '../../management/store/reducer'; import { ManagementPluginReducer } from '../../management'; @@ -52,6 +53,7 @@ const combineTimelineReducer = reduceReducers( export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = { hosts: hostsReducer, network: networkReducer, + ueba: uebaReducer, timeline: combineTimelineReducer, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, diff --git a/x-pack/plugins/security_solution/public/common/store/app/model.ts b/x-pack/plugins/security_solution/public/common/store/app/model.ts index 2888867167c14..2c4ddb703f6a0 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/model.ts @@ -27,5 +27,5 @@ export type ErrorModel = Error[]; export interface AppModel { notesById: NotesById; errors: ErrorState; - enableExperimental?: ExperimentalFeatures; + enableExperimental: ExperimentalFeatures; } diff --git a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts index 20c9b0e14dbd9..5b0a2330a408d 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts @@ -17,6 +17,13 @@ export type AppState = AppModel; export const initialAppState: AppState = { notesById: {}, errors: [], + enableExperimental: { + trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, + ruleRegistryEnabled: false, + tGridEnabled: false, + uebaEnabled: false, + }, }; interface UpdateNotesByIdParams { diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index c2ef2563fe63e..d5633ee84d6d4 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -14,6 +14,7 @@ import { sourcererReducer, sourcererModel } from './sourcerer'; import { HostsPluginReducer } from '../../hosts/store'; import { NetworkPluginReducer } from '../../network/store'; +import { UebaPluginReducer } from '../../ueba/store'; import { TimelinePluginReducer } from '../../timelines/store/timeline'; import { SecuritySubPlugins } from '../../app/types'; @@ -24,6 +25,7 @@ import { KibanaIndexPatterns } from './sourcerer/model'; import { ExperimentalFeatures } from '../../../common/experimental_features'; export type SubPluginsInitReducer = HostsPluginReducer & + UebaPluginReducer & NetworkPluginReducer & TimelinePluginReducer & ManagementPluginReducer; diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 21e833abe1f9b..6943b4cf73117 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -18,10 +18,12 @@ import { HostsPluginState } from '../../hosts/store'; import { DragAndDropState } from './drag_and_drop/reducer'; import { TimelinePluginState } from '../../timelines/store/timeline'; import { NetworkPluginState } from '../../network/store'; +import { UebaPluginState } from '../../ueba/store'; import { ManagementPluginState } from '../../management'; export type StoreState = HostsPluginState & NetworkPluginState & + UebaPluginState & TimelinePluginState & ManagementPluginState & { app: AppState; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/types.ts b/x-pack/plugins/security_solution/public/common/utils/route/types.ts index 189e68d1c55bb..c6d5852881850 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/types.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/types.ts @@ -15,8 +15,14 @@ import { HostsTableType } from '../../../hosts/store/model'; import { NetworkRouteType } from '../../../network/pages/navigation/types'; import { AdministrationSubTab as AdministrationType } from '../../../management/types'; import { FlowTarget } from '../../../../common/search_strategy'; +import { UebaTableType } from '../../../ueba/store/model'; -export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType | AdministrationType; +export type SiemRouteType = + | HostsTableType + | NetworkRouteType + | TimelineType + | AdministrationType + | UebaTableType; export interface RouteSpyState { pageName: string; detailName: string | undefined; @@ -32,6 +38,9 @@ export interface HostRouteSpyState extends RouteSpyState { tabName: HostsTableType | undefined; } +export interface UebaRouteSpyState extends RouteSpyState { + tabName: UebaTableType | undefined; +} export interface NetworkRouteSpyState extends RouteSpyState { tabName: NetworkRouteType | undefined; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9f59e3763ffbc..b1881d29ec10d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -23,7 +23,10 @@ import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { + DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, +} from '../../../../../common/constants'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; @@ -49,6 +52,7 @@ import { AlertData, EcsHit } from '../../../../common/components/exceptions/type import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; interface AlertContextMenuProps { ariaLabel?: string; @@ -84,6 +88,8 @@ const AlertContextMenuComponent: React.FC = ({ [ecsRowData] ); + // TODO: Steph/ueba remove when past experimental + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); const ruleIndices = useMemo((): string[] => { if ( @@ -93,9 +99,11 @@ const AlertContextMenuComponent: React.FC = ({ ) { return ecsRowData.signal.rule.index; } else { - return DEFAULT_INDEX_PATTERN; + return uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN; } - }, [ecsRowData]); + }, [ecsRowData.signal?.rule, uebaEnabled]); const { addWarning } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f641e96e3d2cf..233189a3e8be9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -84,7 +84,11 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { APP_ID, DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; +import { + APP_ID, + DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, +} from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -226,6 +230,9 @@ const RuleDetailsPageComponent = () => { // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + // TODO: Steph/ueba remove when past experimental + const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled'); + // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const { @@ -347,7 +354,14 @@ const RuleDetailsPageComponent = () => { ), [ruleDetailTab, setRuleDetailTab] ); - + const ruleIndices = useMemo( + () => + rule?.index ?? + (uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN), + [rule?.index, uebaEnabled] + ); const handleRefresh = useCallback(() => { if (fetchRuleStatus != null && ruleId != null) { fetchRuleStatus(ruleId); @@ -723,7 +737,7 @@ const RuleDetailsPageComponent = () => { ( export const isDetectionsPath = (pathname: string): boolean => { return !!matchPath(pathname, { - path: `(${ALERTS_PATH}|${RULES_PATH}|${EXCEPTIONS_PATH})`, + path: `(${ALERTS_PATH}|${RULES_PATH}|${UEBA_PATH}|${EXCEPTIONS_PATH})`, strict: false, }); }; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 47026cbec49ad..430c77b9422d8 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -16,6 +16,7 @@ import { Exceptions } from './exceptions'; import { Hosts } from './hosts'; import { Network } from './network'; +import { Ueba } from './ueba'; import { Overview } from './overview'; import { Rules } from './rules'; @@ -31,6 +32,7 @@ const subPluginClasses = { Exceptions, Hosts, Network, + Ueba, Overview, Rules, Timelines, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 137fef1641501..ee5ca84c6e13f 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -58,16 +58,21 @@ import { SecuritySolutionUiConfigType } from './common/types'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; -import { parseExperimentalConfigValue } from '../common/experimental_features'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; import type { TimelineState } from '../../timelines/public'; import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; export class Plugin implements IPlugin { - private kibanaVersion: string; + readonly kibanaVersion: string; private config: SecuritySolutionUiConfigType; + readonly experimentalFeatures: ExperimentalFeatures; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); this.kibanaVersion = initializerContext.env.packageInfo.version; } private appUpdater$ = new Subject(); @@ -151,7 +156,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins); @@ -231,7 +236,11 @@ export class Plugin implements IPlugin ({ navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks(currentLicense.type, core.application.capabilities), + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), })); } }); @@ -239,6 +248,7 @@ export class Plugin implements IPlugin { if (!this._store) { - const experimentalFeatures = parseExperimentalConfigValue( - this.config.enableExperimental || [] - ); const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); const [ { createStore, createInitialState }, @@ -359,7 +370,7 @@ export class Plugin implements IPlugin; hosts: ReturnType; network: ReturnType; + // TODO: Steph/ueba require ueba once no longer experimental + ueba?: ReturnType; overview: ReturnType; timelines: ReturnType; management: ReturnType; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx new file mode 100644 index 0000000000000..4289b7d2c62da --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { HostRulesColumns } from './'; + +import * as i18n from './translations'; +import { HostRulesFields } from '../../../../common'; + +export const getHostRulesColumns = (): HostRulesColumns => [ + { + field: `node.${HostRulesFields.ruleName}`, + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + render: (ruleName) => { + if (ruleName != null && ruleName.length > 0) { + const id = escapeDataProviderId(`ueba-table-ruleName-${ruleName}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + ruleName + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.ruleType}`, + name: i18n.RULE_TYPE, + truncateText: false, + hideForMobile: false, + render: (ruleType) => { + if (ruleType != null && ruleType.length > 0) { + const id = escapeDataProviderId(`ueba-table-ruleType-${ruleType}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + ruleType + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.riskScore}`, + name: i18n.RISK_SCORE, + truncateText: false, + hideForMobile: false, + render: (riskScore) => { + if (riskScore != null) { + const id = escapeDataProviderId(`ueba-table-riskScore-${riskScore}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + riskScore + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostRulesFields.hits}`, + name: i18n.HITS, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (hits) => { + if (hits != null) { + return hits; + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx new file mode 100644 index 0000000000000..3d369a56a7bc0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getHostRulesColumns } from './columns'; +import * as i18n from './translations'; +import { + HostRulesEdges, + HostRulesItem, + HostRulesSortField, + HostRulesFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { HOST_RULES } from '../../pages/translations'; +import { rowItems } from '../utils'; + +interface HostRulesTableProps { + data: HostRulesEdges[]; + fakeTotalCount: number; + headerTitle?: string; + headerSupplement?: React.ReactElement; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: uebaModel.UebaType; + tableType: uebaModel.UebaTableType.hostRules | uebaModel.UebaTableType.userRules; +} + +export type HostRulesColumns = [ + Columns, + Columns, + Columns, + Columns +]; + +const getSorting = (sortField: HostRulesFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const HostRulesTableComponent: React.FC = ({ + data, + fakeTotalCount, + headerTitle, + headerSupplement, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + tableType, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.hostRulesSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [tableType, type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, // this will need to become unique for each user table in the group + }) + ), + [tableType, type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: HostRulesSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateHostRulesSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getHostRulesColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + const headerProps = useMemo( + () => + tableType === uebaModel.UebaTableType.userRules && headerTitle && headerSupplement + ? { + headerTitle, + headerSupplement, + } + : { headerTitle: HOST_RULES }, + [headerSupplement, headerTitle, tableType] + ); + return ( + + ); +}; + +HostRulesTableComponent.displayName = 'HostRulesTableComponent'; + +const getSortField = (field: string): HostRulesFields => { + switch (field) { + case `node.${HostRulesFields.ruleName}`: + return HostRulesFields.ruleName; + case `node.${HostRulesFields.riskScore}`: + return HostRulesFields.riskScore; + default: + return HostRulesFields.riskScore; + } +}; + +const getNodeField = (field: HostRulesFields): string => `node.${field}`; + +export const HostRulesTable = React.memo(HostRulesTableComponent); + +HostRulesTable.displayName = 'HostRulesTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts new file mode 100644 index 0000000000000..f029910b9714b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.uebaTableHostRules.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {rule} other {rules}}`, + }); + +export const NAME = i18n.translate('xpack.securitySolution.uebaTableHostRules.ruleName', { + defaultMessage: 'Rule name', +}); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.uebaTableHostRules.totalRiskScore', + { + defaultMessage: 'Total risk score', + } +); + +export const RULE_TYPE = i18n.translate('xpack.securitySolution.uebaTableHostRules.ruleType', { + defaultMessage: 'Rule type', +}); + +export const HITS = i18n.translate('xpack.securitySolution.uebaTableHostRules.hits', { + defaultMessage: 'Number of hits', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx new file mode 100644 index 0000000000000..19516ad6fcafa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { HostTacticsColumns } from './'; + +import * as i18n from './translations'; +import { HostTacticsFields } from '../../../../common'; + +export const getHostTacticsColumns = (): HostTacticsColumns => [ + { + field: `node.${HostTacticsFields.tactic}`, + name: i18n.TACTIC, + truncateText: false, + hideForMobile: false, + render: (tactic) => { + if (tactic != null && tactic.length > 0) { + const id = escapeDataProviderId(`ueba-table-tactic-${tactic}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + tactic + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.technique}`, + name: i18n.TECHNIQUE, + truncateText: false, + hideForMobile: false, + render: (technique) => { + if (technique != null && technique.length > 0) { + const id = escapeDataProviderId(`ueba-table-technique-${technique}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + technique + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.riskScore}`, + name: i18n.RISK_SCORE, + truncateText: false, + hideForMobile: false, + render: (riskScore) => { + if (riskScore != null) { + const id = escapeDataProviderId(`ueba-table-riskScore-${riskScore}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + riskScore + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: `node.${HostTacticsFields.hits}`, + name: i18n.HITS, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (hits) => { + if (hits != null) { + return hits; + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx new file mode 100644 index 0000000000000..28bd3d6ad43a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getHostTacticsColumns } from './columns'; +import * as i18n from './translations'; +import { + HostTacticsEdges, + HostTacticsItem, + HostTacticsSortField, + HostTacticsFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { HOST_TACTICS } from '../../pages/translations'; +import { rowItems } from '../utils'; + +const tableType = uebaModel.UebaTableType.hostTactics; + +interface HostTacticsTableProps { + data: HostTacticsEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + techniqueCount: number; + totalCount: number; + type: uebaModel.UebaType; +} + +export type HostTacticsColumns = [ + Columns, + Columns, + Columns, + Columns +]; + +const getSorting = (sortField: HostTacticsFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const HostTacticsTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + techniqueCount, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.hostTacticsSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, // this will need to become unique for each user table in the group + }) + ), + [type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: HostTacticsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateHostTacticsSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getHostTacticsColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + return ( + + ); +}; + +HostTacticsTableComponent.displayName = 'HostTacticsTableComponent'; + +const getSortField = (field: string): HostTacticsFields => { + switch (field) { + case `node.${HostTacticsFields.tactic}`: + return HostTacticsFields.tactic; + case `node.${HostTacticsFields.riskScore}`: + return HostTacticsFields.riskScore; + default: + return HostTacticsFields.riskScore; + } +}; + +const getNodeField = (field: HostTacticsFields): string => `node.${field}`; + +export const HostTacticsTable = React.memo(HostTacticsTableComponent); + +HostTacticsTable.displayName = 'HostTacticsTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts new file mode 100644 index 0000000000000..98cd53a59e5f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COUNT = (totalCount: number, techniqueCount: number) => + i18n.translate('xpack.securitySolution.uebaTableHostTactics.tacticTechnique', { + values: { techniqueCount, totalCount }, + defaultMessage: `{totalCount} {totalCount, plural, =1 {tactic} other {tactics}} with {techniqueCount} {techniqueCount, plural, =1 {technique} other {techniques}}`, + }); + +export const TACTIC = i18n.translate('xpack.securitySolution.uebaTableHostTactics.tactic', { + defaultMessage: 'Tactic', +}); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.uebaTableHostTactics.totalRiskScore', + { + defaultMessage: 'Total risk score', + } +); + +export const TECHNIQUE = i18n.translate('xpack.securitySolution.uebaTableHostTactics.technique', { + defaultMessage: 'Technique', +}); + +export const HITS = i18n.translate('xpack.securitySolution.uebaTableHostTactics.hits', { + defaultMessage: 'Number of hits', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx new file mode 100644 index 0000000000000..b751521001fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { UebaDetailsLink } from '../../../common/components/links'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { + AddFilterToGlobalSearchBar, + createFilter, +} from '../../../common/components/add_filter_to_global_search_bar'; +import { RiskScoreColumns } from './'; + +import * as i18n from './translations'; +export const getRiskScoreColumns = (): RiskScoreColumns => [ + { + field: 'node.host_name', + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + sortable: true, + render: (hostName) => { + if (hostName != null && hostName.length > 0) { + const id = escapeDataProviderId(`ueba-table-hostName-${hostName}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'node.risk_keyword', + name: i18n.CURRENT_RISK, + truncateText: false, + hideForMobile: false, + sortable: false, + render: (riskKeyword) => { + if (riskKeyword != null) { + return ( + + <>{riskKeyword} + + ); + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx new file mode 100644 index 0000000000000..9e9c6f81a43bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + Columns, + Criteria, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaActions, uebaModel, uebaSelectors } from '../../store'; +import { getRiskScoreColumns } from './columns'; +import * as i18n from './translations'; +import { + RiskScoreEdges, + RiskScoreItem, + RiskScoreSortField, + RiskScoreFields, +} from '../../../../common'; +import { Direction } from '../../../../common/search_strategy'; +import { rowItems } from '../utils'; + +const tableType = uebaModel.UebaTableType.riskScore; + +interface RiskScoreTableProps { + data: RiskScoreEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: uebaModel.UebaType; +} + +export type RiskScoreColumns = [ + Columns, + Columns +]; + +const getSorting = (sortField: RiskScoreFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const RiskScoreTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const { activePage, limit, sort } = useDeepEqualSelector(uebaSelectors.riskScoreSelector()); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + uebaActions.updateTableLimit({ + uebaType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + uebaActions.updateTableActivePage({ + activePage: newPage, + uebaType: type, + tableType, + }) + ), + [type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newSort: RiskScoreSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (newSort.direction !== sort.direction || newSort.field !== sort.field) { + // dispatch( + // uebaActions.updateRiskScoreSort({ + // sort, + // uebaType: type, + // }) + // ); TODO: Steph/ueba implement sorting + } + } + }, + [sort] + ); + + const columns = useMemo(() => getRiskScoreColumns(), []); + + const sorting = useMemo(() => getSorting(sort.field, sort.direction), [sort]); + + return ( + + ); +}; + +RiskScoreTableComponent.displayName = 'RiskScoreTableComponent'; + +const getSortField = (field: string): RiskScoreFields => { + switch (field) { + case `node.${RiskScoreFields.hostName}`: + return RiskScoreFields.hostName; + case `node.${RiskScoreFields.riskScore}`: + return RiskScoreFields.riskScore; + default: + return RiskScoreFields.riskScore; + } +}; + +const getNodeField = (field: RiskScoreFields): string => `node.${field}`; + +export const RiskScoreTable = React.memo(RiskScoreTableComponent); + +RiskScoreTable.displayName = 'RiskScoreTable'; diff --git a/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts new file mode 100644 index 0000000000000..a4e7a3271d152 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.uebaTableRiskScore.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {user} other {users}}`, + }); + +export const NAME = i18n.translate('xpack.securitySolution.uebaTableRiskScore.nameTitle', { + defaultMessage: 'Host name', +}); + +export const RISK_SCORE = i18n.translate('xpack.securitySolution.uebaTableRiskScore.riskScore', { + defaultMessage: 'Risk score', +}); + +export const CURRENT_RISK = i18n.translate( + 'xpack.securitySolution.uebaTableRiskScore.currentRisk', + { + defaultMessage: 'Current risk', + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/components/translations.ts b/x-pack/plugins/security_solution/public/ueba/components/translations.ts new file mode 100644 index 0000000000000..5775871a3fe4a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ROWS_5 = i18n.translate('xpack.securitySolution.uebaTable.rows', { + values: { numRows: 5 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); + +export const ROWS_10 = i18n.translate('xpack.securitySolution.uebaTable.rows', { + values: { numRows: 10 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/components/utils.ts b/x-pack/plugins/security_solution/public/ueba/components/utils.ts new file mode 100644 index 0000000000000..d12e66a5f6d7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/components/utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ItemsPerRow } from '../../common/components/paginated_table'; +import * as i18n from './translations'; + +export const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx new file mode 100644 index 0000000000000..7db1a77244bbe --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + HostRulesEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + HostRulesRequestOptions, + HostRulesStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'hostRulesQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface HostRulesState { + data: HostRulesEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + totalCount: number; +} + +interface UseHostRules { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useHostRules = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseHostRules): [boolean, HostRulesState] => { + const getHostRulesSelector = useMemo(() => uebaSelectors.hostRulesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getHostRulesSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [hostRulesRequest, setHostRulesRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRulesRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [hostRulesResponse, setHostRulesResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + totalCount: -1, + }); + + const hostRulesSearch = useCallback( + (request: HostRulesRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setHostRulesResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setHostRulesRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.hostRules, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + hostRulesSearch(hostRulesRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [hostRulesRequest, hostRulesSearch]); + + return [loading, hostRulesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx new file mode 100644 index 0000000000000..35dd2a0b08d4e --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + HostTacticsEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'hostTacticsQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface HostTacticsState { + data: HostTacticsEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + techniqueCount: number; + totalCount: number; +} + +interface UseHostTactics { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useHostTactics = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseHostTactics): [boolean, HostTacticsState] => { + const getHostTacticsSelector = useMemo(() => uebaSelectors.hostTacticsSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getHostTacticsSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [hostTacticsRequest, setHostTacticsRequest] = useState( + null + ); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostTacticsRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [hostTacticsResponse, setHostTacticsResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + techniqueCount: -1, + totalCount: -1, + }); + + const hostTacticsSearch = useCallback( + (request: HostTacticsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setHostTacticsResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + techniqueCount: response.techniqueCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setHostTacticsRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.hostTactics, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + hostTacticsSearch(hostTacticsRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [hostTacticsRequest, hostTacticsSearch]); + + return [loading, hostTacticsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx new file mode 100644 index 0000000000000..f2f353ffc0cff --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + RiskScoreEdges, + PageInfoPaginated, + DocValueFields, + UebaQueries, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'riskScoreQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface RiskScoreState { + data: RiskScoreEdges[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: string; + totalCount: number; +} + +interface UseRiskScore { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useRiskScore = ({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip = false, + startDate, +}: UseRiskScore): [boolean, RiskScoreState] => { + const getRiskScoreSelector = useMemo(() => uebaSelectors.riskScoreSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getRiskScoreSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [riskScoreRequest, setRiskScoreRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setRiskScoreRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [riskScoreResponse, setRiskScoreResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + startDate, + totalCount: -1, + }); + + const riskScoreSearch = useCallback( + (request: RiskScoreRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setRiskScoreResponse((prevResponse) => ({ + ...prevResponse, + data: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_RISK_SCORE); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_RISK_SCORE }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setRiskScoreRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.riskScore, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + ]); + + useEffect(() => { + riskScoreSearch(riskScoreRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [riskScoreRequest, riskScoreSearch]); + + return [loading, riskScoreResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts new file mode 100644 index 0000000000000..8cc275674d4e9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.riskScore.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.riskScore.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx new file mode 100644 index 0000000000000..3c4e45bd3a1e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Subscription } from 'rxjs'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { uebaModel, uebaSelectors } from '../../store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + DocValueFields, + UebaQueries, + UserRulesRequestOptions, + UserRulesStrategyResponse, + UserRulesStrategyUserResponse, +} from '../../../../common'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import { useTransforms } from '../../../transforms/containers/use_transforms'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; + +export const ID = 'userRulesQuery'; + +type LoadPage = (newActivePage: number) => void; +export interface UserRulesState { + data: UserRulesStrategyUserResponse[]; + endDate: string; + id: string; + inspect: InspectResponse; + isInspected: boolean; + loadPage: LoadPage; + refetch: inputsModel.Refetch; + startDate: string; +} + +interface UseUserRules { + docValueFields?: DocValueFields[]; + endDate: string; + filterQuery?: ESTermQuery | string; + hostName: string; + indexNames: string[]; + skip?: boolean; + startDate: string; + type: uebaModel.UebaType; +} + +export const useUserRules = ({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip = false, + startDate, +}: UseUserRules): [boolean, UserRulesState] => { + const getUserRulesSelector = useMemo(() => uebaSelectors.userRulesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state: State) => + getUserRulesSelector(state) + ); + const { data } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [userRulesRequest, setUserRulesRequest] = useState(null); + const { getTransformChangesIfTheyExist } = useTransforms(); + const { addError, addWarning } = useAppToasts(); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setUserRulesRequest((prevRequest) => { + if (!prevRequest) { + return prevRequest; + } + + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [userRulesResponse, setUserRulesResponse] = useState({ + data: [], + endDate, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + refetch: refetch.current, + startDate, + }); + + const userRulesSearch = useCallback( + (request: UserRulesRequestOptions | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + searchSubscription.current = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setUserRulesResponse((prevResponse) => ({ + ...prevResponse, + data: response.data, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + searchSubscription.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_HOST_RULES); + searchSubscription.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { title: i18n.FAIL_HOST_RULES }); + searchSubscription.current.unsubscribe(); + }, + }); + setLoading(false); + }; + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, addError, addWarning, skip] + ); + + useEffect(() => { + setUserRulesRequest((prevRequest) => { + const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ + factoryQueryType: UebaQueries.userRules, + indices: indexNames, + filterQuery, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + const myRequest = { + ...(prevRequest ?? {}), + hostName, + defaultIndex: indices, + docValueFields: docValueFields ?? [], + factoryQueryType, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange, + sort, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [ + activePage, + docValueFields, + endDate, + filterQuery, + indexNames, + limit, + startDate, + sort, + getTransformChangesIfTheyExist, + hostName, + ]); + + useEffect(() => { + userRulesSearch(userRulesRequest); + return () => { + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [userRulesRequest, userRulesSearch]); + + return [loading, userRulesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts new file mode 100644 index 0000000000000..6cf5521f4eaaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.errorSearchDescription', + { + defaultMessage: `An error has occurred on risk score search`, + } +); + +export const FAIL_HOST_RULES = i18n.translate( + 'xpack.securitySolution.hostRules.failSearchDescription', + { + defaultMessage: `Failed to run search on risk score`, + } +); diff --git a/x-pack/plugins/security_solution/public/ueba/index.ts b/x-pack/plugins/security_solution/public/ueba/index.ts new file mode 100644 index 0000000000000..030844735b0f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { SecuritySubPluginWithStore } from '../app/types'; +import { routes } from './routes'; +import { initialUebaState, uebaReducer, uebaModel } from './store'; +import { TimelineId } from '../../common/types/timeline'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; + +export class Ueba { + public setup() {} + + public start(storage: Storage): SecuritySubPluginWithStore<'ueba', uebaModel.UebaModel> { + return { + routes, + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, [TimelineId.uebaPageExternalAlerts]), + }, + store: { + initialState: { ueba: initialUebaState }, + reducer: { ueba: uebaReducer }, + }, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx new file mode 100644 index 0000000000000..dad3277d0a7a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../common/components/ml/types'; +import { UebaTableType } from '../../store/model'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; + +import { UebaDetailsTabsProps } from './types'; +import { type } from './utils'; + +import { + HostRulesQueryTabBody, + HostTacticsQueryTabBody, + UserRulesQueryTabBody, +} from '../navigation'; + +export const UebaDetailsTabs = React.memo( + ({ + detailName, + docValueFields, + filterQuery, + indexNames, + indexPattern, + pageFilters, + setAbsoluteRangeDatePicker, + uebaDetailsPagePath, + }) => { + const { from, to, isInitializing, deleteQuery, setQuery } = useGlobalTime(); + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + skip: isInitializing || filterQuery === undefined, + setQuery, + startDate: from, + type, + indexPattern, + indexNames, + hostName: detailName, + narrowDateRange, + updateDateRange, + }; + return ( + + + + + + + + + + + + ); + } +); + +UebaDetailsTabs.displayName = 'UebaDetailsTabs'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts new file mode 100644 index 0000000000000..70f8027b1f55b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { escapeQueryValue } from '../../../common/lib/keury'; +import { Filter } from '../../../../../../../src/plugins/data/public'; + +/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ +export const getUebaDetailsEventsKqlQueryExpression = ({ + filterQueryExpression, + hostName, +}: { + filterQueryExpression: string; + hostName: string; +}): string => { + if (filterQueryExpression.length) { + return `${filterQueryExpression}${ + hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' + }`; + } else { + return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; + } +}; + +export const getUebaDetailsPageFilters = (hostName: string): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: hostName, + params: { + query: hostName, + }, + }, + query: { + match: { + 'host.name': { + query: hostName, + type: 'phrase', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx new file mode 100644 index 0000000000000..5a297099f3834 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +import { LastEventIndexKey } from '../../../../common/search_strategy'; +import { SecurityPageName } from '../../../app/types'; +import { FiltersGlobal } from '../../../common/components/filters_global'; +import { HeaderPage } from '../../../common/components/header_page'; +import { LastEventTime } from '../../../common/components/last_event_time'; +import { SecuritySolutionTabNavigation } from '../../../common/components/navigation'; +import { SiemSearchBar } from '../../../common/components/search_bar'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useKibana } from '../../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { inputsSelectors } from '../../../common/store'; +import { setUebaDetailsTablesActivePageToZero } from '../../store/actions'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; + +import { OverviewEmpty } from '../../../overview/components/overview_empty'; +import { UebaDetailsTabs } from './details_tabs'; +import { navTabsUebaDetails } from './nav_tabs'; +import { UebaDetailsProps } from './types'; +import { type } from './utils'; +import { getUebaDetailsPageFilters } from './helpers'; +import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; +import { Display } from '../display'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../common/types/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +const ID = 'UebaDetailsQueryId'; + +const UebaDetailsComponent: React.FC = ({ detailName, uebaDetailsPagePath }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useGlobalFullScreen(); + + const kibana = useKibana(); + const uebaDetailsPageFilters: Filter[] = useMemo(() => getUebaDetailsPageFilters(detailName), [ + detailName, + ]); + const getFilters = () => [...uebaDetailsPageFilters, ...filters]; + + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope( + SourcererScopeName.detections + ); + + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); + + useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); + + useEffect(() => { + dispatch(setUebaDetailsTablesActivePageToZero()); + }, [dispatch, detailName]); + + return ( + <> + {indicesExist ? ( + <> + + + + + + + + + } + title={detailName} + /> + + + + + + + + + ) : ( + + + + + + )} + + + + ); +}; + +UebaDetailsComponent.displayName = 'UebaDetailsComponent'; + +export const UebaDetails = React.memo(UebaDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx new file mode 100644 index 0000000000000..ba97a03bf6daf --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from '../translations'; +import { UebaDetailsNavTab } from './types'; +import { UebaTableType } from '../../store/model'; +import { UEBA_PATH } from '../../../../common/constants'; + +const getTabsOnUebaDetailsUrl = (hostName: string, tabName: UebaTableType) => + `${UEBA_PATH}/${hostName}/${tabName}`; + +export const navTabsUebaDetails = (hostName: string): UebaDetailsNavTab => { + return { + [UebaTableType.hostRules]: { + id: UebaTableType.hostRules, + name: i18n.HOST_RULES, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.hostRules), + disabled: false, + }, + [UebaTableType.hostTactics]: { + id: UebaTableType.hostTactics, + name: i18n.HOST_TACTICS, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.hostTactics), + disabled: false, + }, + [UebaTableType.userRules]: { + id: UebaTableType.userRules, + name: i18n.USER_RULES, + href: getTabsOnUebaDetailsUrl(hostName, UebaTableType.userRules), + disabled: false, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts new file mode 100644 index 0000000000000..976b033db5f5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/types.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionCreator } from 'typescript-fsa'; +import { Query, IIndexPattern, Filter } from 'src/plugins/data/public'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import { UebaTableType } from '../../store/model'; +import { UebaQueryProps } from '../types'; +import { NavTab } from '../../../common/components/navigation/types'; +import { uebaModel } from '../../store'; +import { DocValueFields } from '../../../common/containers/source'; + +interface UebaDetailsComponentReduxProps { + query: Query; + filters: Filter[]; +} + +interface HostBodyComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: string; + to: string; + }>; + detailName: string; + uebaDetailsPagePath: string; +} + +interface UebaDetailsComponentDispatchProps extends HostBodyComponentDispatchProps { + setUebaDetailsTablesActivePageToZero: ActionCreator; +} + +export interface UebaDetailsProps { + detailName: string; + uebaDetailsPagePath: string; +} + +export type UebaDetailsComponentProps = UebaDetailsComponentReduxProps & + UebaDetailsComponentDispatchProps & + UebaQueryProps; + +type KeyUebaDetailsNavTab = UebaTableType.hostRules & + UebaTableType.hostTactics & + UebaTableType.userRules; + +export type UebaDetailsNavTab = Record; + +export type UebaDetailsTabsProps = HostBodyComponentDispatchProps & + UebaQueryProps & { + docValueFields?: DocValueFields[]; + indexNames: string[]; + pageFilters?: Filter[]; + filterQuery?: string; + indexPattern: IIndexPattern; + type: uebaModel.UebaType; + }; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: string; + to: string; +}>; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts b/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts new file mode 100644 index 0000000000000..d5f346d3ece64 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { uebaModel } from '../../store'; +import { UebaTableType } from '../../store/model'; +import { getUebaDetailsUrl } from '../../../common/components/link_to/redirect_to_ueba'; + +import * as i18n from '../translations'; +import { UebaRouteSpyState } from '../../../common/utils/route/types'; +import { GetUrlForApp } from '../../../common/components/navigation/types'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; + +export const type = uebaModel.UebaType.details; + +const TabNameMappedToI18nKey: Record = { + [UebaTableType.hostRules]: i18n.HOST_RULES, + [UebaTableType.hostTactics]: i18n.HOST_TACTICS, + [UebaTableType.riskScore]: i18n.RISK_SCORE_TITLE, + [UebaTableType.userRules]: i18n.USER_RULES, +}; + +export const getBreadcrumbs = ( + params: UebaRouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getUrlForApp(APP_ID, { + path: !isEmpty(search[0]) ? search[0] : '', + deepLinkId: SecurityPageName.ueba, + }), + }, + ]; + + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: getUrlForApp(APP_ID, { + path: getUebaDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + deepLinkId: SecurityPageName.ueba, + }), + }, + ]; + } + + if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/display.tsx b/x-pack/plugins/security_solution/public/ueba/pages/display.tsx new file mode 100644 index 0000000000000..a907f1fdb5997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/display.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const Display = styled.div<{ show: boolean }>` + ${({ show }) => (show ? '' : 'display: none;')}; +`; + +Display.displayName = 'Display'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/index.tsx new file mode 100644 index 0000000000000..c4a6794b75999 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import { UEBA_PATH } from '../../../common/constants'; +import { UebaTableType } from '../store/model'; +import { Ueba } from './ueba'; +import { uebaDetailsPagePath } from './types'; +import { UebaDetails } from './details'; + +const uebaTabPath = `${UEBA_PATH}/:tabName(${UebaTableType.riskScore})`; + +const uebaDetailsTabPath = + `${uebaDetailsPagePath}/:tabName(` + + `${UebaTableType.hostRules}|` + + `${UebaTableType.hostTactics}|` + + `${UebaTableType.userRules})`; + +export const UebaContainer = React.memo(() => ( + + ( + + )} + /> + + + + + } + /> + ( + + )} + /> + +)); + +UebaContainer.displayName = 'UebaContainer'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx new file mode 100644 index 0000000000000..5e06e5c9bf068 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as i18n from './translations'; +import { UebaTableType } from '../store/model'; +import { UebaNavTab } from './navigation/types'; +import { UEBA_PATH } from '../../../common/constants'; + +const getTabsOnUebaUrl = (tabName: UebaTableType) => `${UEBA_PATH}/${tabName}`; + +export const navTabsUeba: UebaNavTab = { + [UebaTableType.riskScore]: { + id: UebaTableType.riskScore, + name: i18n.RISK_SCORE_TITLE, + href: getTabsOnUebaUrl(UebaTableType.riskScore), + disabled: false, + }, +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx new file mode 100644 index 0000000000000..bce19a9da7ab9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { useHostRules } from '../../containers/host_rules'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostRulesTable } from '../../components/host_rules_table'; +import { uebaModel } from '../../store'; + +const HostRulesTableManage = manageQuery(HostRulesTable); + +export const HostRulesQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [ + loading, + { data, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useHostRules({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + + return ( + + ); +}; + +HostRulesQueryTabBody.displayName = 'HostRulesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx new file mode 100644 index 0000000000000..c441eff3219d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { useHostTactics } from '../../containers/host_tactics'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostTacticsTable } from '../../components/host_tactics_table'; + +const HostTacticsTableManage = manageQuery(HostTacticsTable); + +export const HostTacticsQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [ + loading, + { data, techniqueCount, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useHostTactics({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + + return ( + + ); +}; + +HostTacticsQueryTabBody.displayName = 'HostTacticsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts new file mode 100644 index 0000000000000..dd549659a3eab --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './host_rules_query_tab_body'; +export * from './host_tactics_query_tab_body'; +export * from './risk_score_query_tab_body'; +export * from './user_rules_query_tab_body'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx new file mode 100644 index 0000000000000..cde972d8a66ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { useRiskScore } from '../../containers/risk_score'; +import { RiskScoreQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { RiskScoreTable } from '../../components/risk_score_table'; + +const RiskScoreTableManage = manageQuery(RiskScoreTable); + +export const RiskScoreQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + indexNames, + skip, + setQuery, + startDate, + type, +}: RiskScoreQueryProps) => { + const [ + loading, + { data, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useRiskScore({ docValueFields, endDate, filterQuery, indexNames, skip, startDate, type }); + + return ( + + ); +}; + +RiskScoreQueryTabBody.displayName = 'RiskScoreQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts new file mode 100644 index 0000000000000..e24b3271cf534 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UebaTableType, UebaType } from '../../store/model'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { DocValueFields } from '../../../../../timelines/common'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { NarrowDateRange } from '../../../common/components/ml/types'; +import { NavTab } from '../../../common/components/navigation/types'; + +type KeyUebaNavTab = UebaTableType.riskScore; + +export type UebaNavTab = Record; +export interface QueryTabBodyProps { + type: UebaType; + startDate: GlobalTimeArgs['from']; + endDate: GlobalTimeArgs['to']; + filterQuery?: string | ESTermQuery; +} + +export type RiskScoreQueryProps = QueryTabBodyProps & { + deleteQuery?: GlobalTimeArgs['deleteQuery']; + docValueFields?: DocValueFields[]; + indexNames: string[]; + pageFilters?: Filter[]; + skip: boolean; + setQuery: GlobalTimeArgs['setQuery']; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; +}; +export type HostQueryProps = RiskScoreQueryProps & { + hostName: string; +}; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx b/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx new file mode 100644 index 0000000000000..f7542b7b4b8a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUserRules } from '../../containers/user_rules'; +import { HostQueryProps } from './types'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { HostRulesTable } from '../../components/host_rules_table'; +import { uebaModel } from '../../store'; +import { UserRulesFields } from '../../../../common'; + +const UserRulesTableManage = manageQuery(HostRulesTable); + +export const UserRulesQueryTabBody = ({ + deleteQuery, + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + setQuery, + startDate, + type, +}: HostQueryProps) => { + const [loading, { data, loadPage, id, inspect, isInspected, refetch }] = useUserRules({ + docValueFields, + endDate, + filterQuery, + hostName, + indexNames, + skip, + startDate, + type, + }); + return ( + + {data.map((user, i) => ( + + {`Total user risk score: ${user[UserRulesFields.riskScore]}`}

} + headerTitle={`user.name: ${user[UserRulesFields.userName]}`} + fakeTotalCount={getOr(50, 'fakeTotalCount', user.pageInfo)} + id={`${id}${i}`} + inspect={inspect} + isInspect={isInspected} + loading={loading} + loadPage={loadPage} + refetch={refetch} + setQuery={setQuery} + showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', user.pageInfo)} + tableType={uebaModel.UebaTableType.userRules} // pagination will not work until this is unique + totalCount={user.totalCount} + type={type} + /> +
+ ))} +
+ ); +}; + +UserRulesQueryTabBody.displayName = 'UserRulesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/translations.ts b/x-pack/plugins/security_solution/public/ueba/pages/translations.ts new file mode 100644 index 0000000000000..0e6519d9d45ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.securitySolution.ueba.pageTitle', { + defaultMessage: 'Users & Entities', +}); +export const RISK_SCORE_TITLE = i18n.translate('xpack.securitySolution.ueba.riskScore', { + defaultMessage: 'Risk score', +}); + +export const HOST_RULES = i18n.translate('xpack.securitySolution.ueba.hostRules', { + defaultMessage: 'Host risk score by rule', +}); + +export const HOST_TACTICS = i18n.translate('xpack.securitySolution.ueba.hostTactics', { + defaultMessage: 'Host risk score by tactic', +}); + +export const USER_RULES = i18n.translate('xpack.securitySolution.ueba.userRules', { + defaultMessage: 'User risk score by rule', +}); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/types.ts b/x-pack/plugins/security_solution/public/ueba/pages/types.ts new file mode 100644 index 0000000000000..07c4d5fccd066 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ActionCreator } from 'typescript-fsa'; + +import { GlobalTimeArgs } from '../../common/containers/use_global_time'; +import { UEBA_PATH } from '../../../common/constants'; +import { uebaModel } from '../../ueba/store'; +import { DocValueFields } from '../../../../timelines/common'; +import { InputsModelId } from '../../common/store/inputs/constants'; + +export const uebaDetailsPagePath = `${UEBA_PATH}/:detailName`; + +export type UebaTabsProps = GlobalTimeArgs & { + docValueFields: DocValueFields[]; + filterQuery: string; + indexNames: string[]; + type: uebaModel.UebaType; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: string; + to: string; + }>; +}; + +export type UebaQueryProps = GlobalTimeArgs; diff --git a/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx b/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx new file mode 100644 index 0000000000000..4e0041a98454c --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import styled from 'styled-components'; +import { noop } from 'lodash/fp'; +import React, { useCallback, useMemo, useRef } from 'react'; +import { isTab } from '../../../../timelines/public'; + +import { SecurityPageName } from '../../app/types'; +import { FiltersGlobal } from '../../common/components/filters_global'; +import { HeaderPage } from '../../common/components/header_page'; +import { LastEventTime } from '../../common/components/last_event_time'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; + +import { SiemSearchBar } from '../../common/components/search_bar'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; +import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; +import { useGlobalTime } from '../../common/containers/use_global_time'; +import { TimelineId } from '../../../common'; +import { LastEventIndexKey } from '../../../common/search_strategy'; +import { useKibana } from '../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../common/lib/keury'; +import { inputsSelectors } from '../../common/store'; +import { setAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; + +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { esQuery } from '../../../../../../src/plugins/data/public'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; +import { Display } from './display'; +import { UebaTabs } from './ueba_tabs'; +import { navTabsUeba } from './nav_tabs'; +import * as i18n from './translations'; +import { uebaModel } from '../store'; +import { + onTimelineTabKeyPressed, + resetKeyboardFocus, + showGlobalFilters, +} from '../../timelines/components/timeline/helpers'; +import { timelineSelectors } from '../../timelines/store/timeline'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector'; +import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query'; + +const ID = 'UebaQueryId'; + +/** + * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. + */ +const StyledFullHeightContainer = styled.div` + display: flex; + flex-direction: column; + flex: 1 1 auto; +`; + +const UebaComponent = () => { + const containerElement = useRef(null); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => + (getTimeline(state, TimelineId.uebaPageExternalAlerts) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useGlobalFullScreen(); + const { uiSettings } = useKibana().services; + const tabsFilters = filters; + + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); + const [filterQuery, kqlError] = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters, + }), + [filters, indexPattern, uiSettings, query] + ); + const [tabsFilterQuery] = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }), + [indexPattern, query, tabsFilters, uiSettings] + ); + + useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); + + const onSkipFocusBeforeEventsTable = useCallback(() => { + containerElement.current + ?.querySelector('.inspectButtonComponent:last-of-type') + ?.focus(); + }, [containerElement]); + + const onSkipFocusAfterEventsTable = useCallback(() => { + resetKeyboardFocus(); + }, []); + + const onKeyDown = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + if (isTab(keyboardEvent)) { + onTimelineTabKeyPressed({ + containerElement: containerElement.current, + keyboardEvent, + onSkipFocusBeforeEventsTable, + onSkipFocusAfterEventsTable, + }); + } + }, + [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] + ); + + return ( + <> + {indicesExist ? ( + + + + + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + + + + ) : ( + + + + + + )} + + + + ); +}; +UebaComponent.displayName = 'UebaComponent'; + +export const Ueba = React.memo(UebaComponent); diff --git a/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx b/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx new file mode 100644 index 0000000000000..b6ae4419b609a --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { UebaTabsProps } from './types'; +import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../common/components/ml/types'; +import { UebaTableType } from '../store/model'; +import { UpdateDateRange } from '../../common/components/charts/common'; +import { UEBA_PATH } from '../../../common/constants'; +import { RiskScoreQueryTabBody } from './navigation'; + +export const UebaTabs = memo( + ({ + deleteQuery, + docValueFields, + filterQuery, + from, + indexNames, + isInitializing, + setAbsoluteRangeDatePicker, + setQuery, + to, + type, + }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + indexNames, + skip: isInitializing || filterQuery === undefined, + setQuery, + startDate: from, + type, + narrowDateRange, + updateDateRange, + }; + + return ( + + + + + + ); + } +); + +UebaTabs.displayName = 'UebaTabs'; diff --git a/x-pack/plugins/security_solution/public/ueba/routes.tsx b/x-pack/plugins/security_solution/public/ueba/routes.tsx new file mode 100644 index 0000000000000..4d761856155e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/routes.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { UebaContainer } from './pages'; + +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; +import { UEBA_PATH } from '../../common/constants'; + +export const UebaRoutes = () => ( + + + +); + +export const routes: SecuritySubPluginRoutes = [ + { + path: UEBA_PATH, + render: UebaRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/ueba/store/actions.ts b/x-pack/plugins/security_solution/public/ueba/store/actions.ts new file mode 100644 index 0000000000000..72ec2ff425d20 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import { uebaModel } from '.'; + +const actionCreator = actionCreatorFactory('x-pack/security_solution/local/ueba'); + +export const updateUebaTable = actionCreator<{ + uebaType: uebaModel.UebaType; + tableType: uebaModel.UebaTableType | uebaModel.UebaTableType; + updates: uebaModel.TableUpdates; +}>('UPDATE_NETWORK_TABLE'); + +export const setUebaDetailsTablesActivePageToZero = actionCreator( + 'SET_UEBA_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' +); + +export const setUebaTablesActivePageToZero = actionCreator('SET_UEBA_TABLES_ACTIVE_PAGE_TO_ZERO'); + +export const updateTableLimit = actionCreator<{ + uebaType: uebaModel.UebaType; + limit: number; + tableType: uebaModel.UebaTableType; +}>('UPDATE_UEBA_TABLE_LIMIT'); + +export const updateTableActivePage = actionCreator<{ + uebaType: uebaModel.UebaType; + activePage: number; + tableType: uebaModel.UebaTableType; +}>('UPDATE_UEBA_ACTIVE_PAGE'); diff --git a/x-pack/plugins/security_solution/public/ueba/store/helpers.ts b/x-pack/plugins/security_solution/public/ueba/store/helpers.ts new file mode 100644 index 0000000000000..653cf30fac484 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/helpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UebaModel, UebaType, UebaTableType, UebaQueries, UebaDetailsQueries } from './model'; +import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; + +export const setUebaPageQueriesActivePageToZero = (state: UebaModel): UebaQueries => ({ + ...state.page.queries, + [UebaTableType.riskScore]: { + ...state.page.queries[UebaTableType.riskScore], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setUebaDetailsQueriesActivePageToZero = (state: UebaModel): UebaDetailsQueries => ({ + ...state.details.queries, + [UebaTableType.hostRules]: { + ...state.details.queries[UebaTableType.hostRules], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [UebaTableType.hostTactics]: { + ...state.details.queries[UebaTableType.hostTactics], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [UebaTableType.userRules]: { + ...state.details.queries[UebaTableType.userRules], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setUebaQueriesActivePageToZero = ( + state: UebaModel, + type: UebaType +): UebaQueries | UebaDetailsQueries => { + if (type === UebaType.page) { + return setUebaPageQueriesActivePageToZero(state); + } else if (type === UebaType.details) { + return setUebaDetailsQueriesActivePageToZero(state); + } + throw new Error(`UebaType ${type} is unknown`); +}; diff --git a/x-pack/plugins/security_solution/public/ueba/store/index.ts b/x-pack/plugins/security_solution/public/ueba/store/index.ts new file mode 100644 index 0000000000000..8538509e58d4b --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Reducer, AnyAction } from 'redux'; +import * as uebaActions from './actions'; +import * as uebaModel from './model'; +import * as uebaSelectors from './selectors'; + +export { uebaActions, uebaModel, uebaSelectors }; +export * from './reducer'; + +export interface UebaPluginState { + ueba: uebaModel.UebaModel; +} + +export interface UebaPluginReducer { + ueba: Reducer; +} diff --git a/x-pack/plugins/security_solution/public/ueba/store/model.ts b/x-pack/plugins/security_solution/public/ueba/store/model.ts new file mode 100644 index 0000000000000..9e9f39977c8ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/model.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + HostRulesSortField, + HostTacticsSortField, + RiskScoreFields, + RiskScoreSortField, + SortField, + UserRulesSortField, +} from '../../../common/search_strategy'; + +export enum UebaType { + page = 'page', + details = 'details', +} + +export enum UebaTableType { + riskScore = 'riskScore', + hostRules = 'hostRules', + hostTactics = 'hostTactics', + userRules = 'userRules', +} + +export type AllUebaTables = UebaTableType; + +export interface BasicQueryPaginated { + activePage: number; + limit: number; +} + +// Ueba Page Models +export interface RiskScoreQuery extends BasicQueryPaginated { + sort: RiskScoreSortField; +} +export interface HostRulesQuery extends BasicQueryPaginated { + sort: HostRulesSortField; +} +export interface UserRulesQuery extends BasicQueryPaginated { + sort: UserRulesSortField; +} +export interface HostTacticsQuery extends BasicQueryPaginated { + sort: HostTacticsSortField; +} + +export interface TableUpdates { + activePage?: number; + limit?: number; + isPtrIncluded?: boolean; + sort?: SortField; +} + +export interface UebaQueries { + [UebaTableType.riskScore]: RiskScoreQuery; +} + +export interface UebaPageModel { + queries: UebaQueries; +} + +export interface UebaDetailsQueries { + [UebaTableType.hostRules]: HostRulesQuery; + [UebaTableType.hostTactics]: HostTacticsQuery; + [UebaTableType.userRules]: UserRulesQuery; +} + +export interface UebaDetailsModel { + queries: UebaDetailsQueries; +} + +export interface UebaModel { + [UebaType.page]: UebaPageModel; + [UebaType.details]: UebaDetailsModel; +} diff --git a/x-pack/plugins/security_solution/public/ueba/store/reducer.ts b/x-pack/plugins/security_solution/public/ueba/store/reducer.ts new file mode 100644 index 0000000000000..f981868c21eb1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/reducer.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { get } from 'lodash/fp'; +import { + Direction, + HostRulesFields, + HostTacticsFields, + RiskScoreFields, +} from '../../../common/search_strategy'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; + +import { + setUebaDetailsTablesActivePageToZero, + setUebaTablesActivePageToZero, + updateUebaTable, + updateTableActivePage, + updateTableLimit, +} from './actions'; +import { + setUebaDetailsQueriesActivePageToZero, + setUebaPageQueriesActivePageToZero, +} from './helpers'; +import { UebaTableType, UebaModel } from './model'; + +export const initialUebaState: UebaModel = { + page: { + queries: { + [UebaTableType.riskScore]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: RiskScoreFields.riskScore, + direction: Direction.desc, + }, + }, + }, + }, + details: { + queries: { + [UebaTableType.hostRules]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostRulesFields.riskScore, + direction: Direction.desc, + }, + }, + [UebaTableType.hostTactics]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostTacticsFields.riskScore, + direction: Direction.desc, + }, + }, + [UebaTableType.userRules]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: HostRulesFields.riskScore, // this looks wrong but its right, the user "table" is an array of host tables + direction: Direction.desc, + }, + }, + }, + }, +}; + +export const uebaReducer = reducerWithInitialState(initialUebaState) + .case(updateUebaTable, (state, { uebaType, tableType, updates }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + ...get([uebaType, 'queries', tableType], state), + ...updates, + }, + }, + }, + })) + .case(setUebaTablesActivePageToZero, (state) => ({ + ...state, + page: { + ...state.page, + queries: setUebaPageQueriesActivePageToZero(state), + }, + details: { + ...state.details, + queries: setUebaDetailsQueriesActivePageToZero(state), + }, + })) + .case(setUebaDetailsTablesActivePageToZero, (state) => ({ + ...state, + details: { + ...state.details, + queries: setUebaDetailsQueriesActivePageToZero(state), + }, + })) + .case(updateTableActivePage, (state, { activePage, uebaType, tableType }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + // TODO: Steph/ueba fix active page/limit on ueba tables. is broken because multiple UebaTableType.userRules tables + // @ts-ignore + ...state[uebaType].queries[tableType], + activePage, + }, + }, + }, + })) + .case(updateTableLimit, (state, { limit, uebaType, tableType }) => ({ + ...state, + [uebaType]: { + ...state[uebaType], + queries: { + ...state[uebaType].queries, + [tableType]: { + // TODO: Steph/ueba fix active page/limit on ueba tables. is broken because multiple UebaTableType.userRules tables + // @ts-ignore + ...state[uebaType].queries[tableType], + limit, + }, + }, + }, + })) + .build(); diff --git a/x-pack/plugins/security_solution/public/ueba/store/selectors.ts b/x-pack/plugins/security_solution/public/ueba/store/selectors.ts new file mode 100644 index 0000000000000..a3d7a5f8a8867 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ueba/store/selectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; + +import { State } from '../../common/store/types'; + +import { UebaDetailsModel, UebaPageModel, UebaTableType } from './model'; + +const selectUebaPage = (state: State): UebaPageModel => state.ueba.page; +const selectUebaDetailsPage = (state: State): UebaDetailsModel => state.ueba.details; + +export const riskScoreSelector = () => + createSelector(selectUebaPage, (ueba) => ueba.queries[UebaTableType.riskScore]); + +export const hostRulesSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.hostRules]); + +export const hostTacticsSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.hostTactics]); + +export const userRulesSelector = () => + createSelector(selectUebaDetailsPage, (ueba) => ueba.queries[UebaTableType.userRules]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index a1d7d03f313db..e98e9b49b3646 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -16,6 +16,7 @@ import { getIndexVersion } from '../../routes/index/get_index_version'; import { SIGNALS_TEMPLATE_VERSION } from '../../routes/index/get_signals_template'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; jest.mock('../../routes/index/get_index_version'); @@ -73,6 +74,7 @@ describe('eql_executor', () => { rule: eqlSO, tuple, exceptionItems, + experimentalFeatures: allowedExperimentalValues, services: alertServices, version, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index e08f519e9761a..8d19510c63477 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -34,11 +34,13 @@ import { SimpleHit, } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const eqlExecutor = async ({ rule, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -50,6 +52,7 @@ export const eqlExecutor = async ({ rule: SavedObject>; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; logger: Logger; @@ -85,7 +88,12 @@ export const eqlExecutor = async ({ throw err; } } - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const request = buildEqlSearchRequest( ruleParams.query, inputIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index 385c01c2f1cda..454cb464506a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -21,12 +21,14 @@ import { AlertAttributes, RuleRangeTuple, BulkCreate, WrapHits } from '../types' import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const queryExecutor = async ({ rule, tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -40,6 +42,7 @@ export const queryExecutor = async ({ tuple: RuleRangeTuple; listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; searchAfterSize: number; @@ -50,7 +53,12 @@ export const queryExecutor = async ({ wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const esFilter = await getFilter({ type: ruleParams.type, filters: ruleParams.filters, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts index d0e22f696b222..37b2c53636cfd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts @@ -20,6 +20,7 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { createThreatSignals } from '../threat_mapping/create_threat_signals'; import { ThreatRuleParams } from '../../schemas/rule_schemas'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const threatMatchExecutor = async ({ rule, @@ -31,6 +32,7 @@ export const threatMatchExecutor = async ({ searchAfterSize, logger, eventsTelemetry, + experimentalFeatures, buildRuleMessage, bulkCreate, wrapHits, @@ -44,12 +46,18 @@ export const threatMatchExecutor = async ({ searchAfterSize: number; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; + experimentalFeatures: ExperimentalFeatures; buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; }) => { const ruleParams = rule.attributes.params; - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); return createThreatSignals({ tuple, threatMapping: ruleParams.threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index 3906c66922238..afcb3707591fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -16,6 +16,7 @@ import { getEntryListMock } from '../../../../../../lists/common/schemas/types/e import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; import { buildRuleMessageFactory } from '../rule_messages'; import { sampleEmptyDocSearchResults } from '../__mocks__/es_results'; +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; describe('threshold_executor', () => { const version = '8.0.0'; @@ -70,6 +71,7 @@ describe('threshold_executor', () => { rule: thresholdSO, tuple, exceptionItems, + experimentalFeatures: allowedExperimentalValues, services: alertServices, version, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index 378d68fc13d2a..ffd90f3b90b91 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -36,11 +36,13 @@ import { mergeReturns, } from '../utils'; import { BuildRuleMessage } from '../rule_messages'; +import { ExperimentalFeatures } from '../../../../../common/experimental_features'; export const thresholdExecutor = async ({ rule, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -52,6 +54,7 @@ export const thresholdExecutor = async ({ rule: SavedObject>; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; + experimentalFeatures: ExperimentalFeatures; services: AlertServices; version: string; logger: Logger; @@ -68,7 +71,12 @@ export const thresholdExecutor = async ({ ); result.warning = true; } - const inputIndex = await getInputIndex(services, version, ruleParams.index); + const inputIndex = await getInputIndex({ + experimentalFeatures, + services, + version, + index: ruleParams.index, + }); const { thresholdSignalHistory, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index 9c4bf37aca789..5058056b169a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -7,7 +7,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { getInputIndex } from './get_input_output_index'; +import { getInputIndex, GetInputIndex } from './get_input_output_index'; describe('get_input_output_index', () => { let servicesMock: AlertServicesMock; @@ -19,7 +19,7 @@ describe('get_input_output_index', () => { afterAll(() => { jest.resetAllMocks(); }); - + let defaultProps: GetInputIndex; beforeEach(() => { servicesMock = alertsMock.createAlertServices(); servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ @@ -28,6 +28,18 @@ describe('get_input_output_index', () => { references: [], attributes: {}, })); + defaultProps = { + services: servicesMock, + version: '8.0.0', + index: ['test-input-index-1'], + experimentalFeatures: { + trustedAppsByPolicyEnabled: false, + metricsEntitiesEnabled: false, + ruleRegistryEnabled: false, + tGridEnabled: false, + uebaEnabled: false, + }, + }; }); describe('getInputOutputIndex', () => { @@ -38,7 +50,7 @@ describe('get_input_output_index', () => { references: [], attributes: {}, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); + const inputIndex = await getInputIndex(defaultProps); expect(inputIndex).toEqual(['test-input-index-1']); }); @@ -51,7 +63,10 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); @@ -64,7 +79,10 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); @@ -77,7 +95,26 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: null, }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); + expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); + }); + + test('Returns a saved object inputIndex default along with experimental features when uebaEnabled=true', async () => { + servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ + id, + type, + references: [], + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); @@ -90,17 +127,26 @@ describe('get_input_output_index', () => { [DEFAULT_INDEX_KEY]: null, }, })); - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: undefined, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { - const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); + const inputIndex = await getInputIndex({ + ...defaultProps, + index: null, + }); expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts index f0c62bee7aec9..d3b60f1e9a281 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts @@ -5,20 +5,33 @@ * 2.0. */ -import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, +} from '../../../../common/constants'; import { AlertInstanceContext, AlertInstanceState, AlertServices, } from '../../../../../alerting/server'; +import { ExperimentalFeatures } from '../../../../common/experimental_features'; + +export interface GetInputIndex { + experimentalFeatures: ExperimentalFeatures; + index: string[] | null | undefined; + services: AlertServices; + version: string; +} -export const getInputIndex = async ( - services: AlertServices, - version: string, - inputIndex: string[] | null | undefined -): Promise => { - if (inputIndex != null) { - return inputIndex; +export const getInputIndex = async ({ + experimentalFeatures, + index, + services, + version, +}: GetInputIndex): Promise => { + if (index != null) { + return index; } else { const configuration = await services.savedObjectsClient.get<{ 'securitySolution:defaultIndex': string[]; @@ -26,7 +39,9 @@ export const getInputIndex = async ( if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { - return DEFAULT_INDEX_PATTERN; + return experimentalFeatures.uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN; } } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index aec8b6c552b1d..a14c678d27536 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -33,6 +33,7 @@ import { queryExecutor } from './executors/query'; import { mlExecutor } from './executors/ml'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -188,6 +189,7 @@ describe('signal_rule_alert_type', () => { payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alert = signalRulesAlertType({ + experimentalFeatures: allowedExperimentalValues, logger, eventsTelemetry: undefined, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 6eef97b05b697..d524757b7c144 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -69,10 +69,12 @@ import { bulkCreateFactory } from './bulk_create_factory'; import { wrapHitsFactory } from './wrap_hits_factory'; import { wrapSequencesFactory } from './wrap_sequences_factory'; import { ConfigType } from '../../../config'; +import { ExperimentalFeatures } from '../../../../common/experimental_features'; export const signalRulesAlertType = ({ logger, eventsTelemetry, + experimentalFeatures, version, ml, lists, @@ -80,6 +82,7 @@ export const signalRulesAlertType = ({ }: { logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; + experimentalFeatures: ExperimentalFeatures; version: string; ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; @@ -153,7 +156,12 @@ export const signalRulesAlertType = ({ if (!isMachineLearningParams(params)) { const index = params.index; const hasTimestampOverride = timestampOverride != null && !isEmpty(timestampOverride); - const inputIndices = await getInputIndex(services, version, index); + const inputIndices = await getInputIndex({ + services, + version, + index, + experimentalFeatures, + }); const [privileges, timestampFieldCaps] = await Promise.all([ checkPrivileges(services, inputIndices), services.scopedClusterClient.asCurrentUser.fieldCaps({ @@ -268,6 +276,7 @@ export const signalRulesAlertType = ({ rule: thresholdRuleSO, tuple, exceptionItems, + experimentalFeatures, services, version, logger, @@ -285,6 +294,7 @@ export const signalRulesAlertType = ({ tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -303,6 +313,7 @@ export const signalRulesAlertType = ({ tuple, listClient, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, @@ -320,6 +331,7 @@ export const signalRulesAlertType = ({ rule: eqlRuleSO, tuple, exceptionItems, + experimentalFeatures, services, version, searchAfterSize, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 1761f3979d7c9..dd6081b6c9127 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -301,6 +301,7 @@ export class Plugin implements IPlugin > = { ...hostsFactory, + ...uebaFactory, ...matrixHistogramFactory, ...networkFactory, ...ctiFactoryTypes, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts new file mode 100644 index 0000000000000..f9c94eea3ff29 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { HostRulesHit, HostRulesEdges, HostRulesFields } from '../../../../../../common'; + +export const formatHostRulesData = (buckets: HostRulesHit[]): HostRulesEdges[] => + buckets.map((bucket) => ({ + node: { + _id: bucket.key, + [HostRulesFields.hits]: bucket.doc_count, + [HostRulesFields.riskScore]: getOr(0, 'risk_score.value', bucket), + [HostRulesFields.ruleName]: bucket.key, + [HostRulesFields.ruleType]: getOr(0, 'rule_type.buckets[0].key', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts new file mode 100644 index 0000000000000..39fa7193fd5d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + HostRulesEdges, + HostRulesRequestOptions, + HostRulesStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildHostRulesQuery } from './query.host_rules.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatHostRulesData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const hostRules: SecuritySolutionFactory = { + buildDsl: (options: HostRulesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildHostRulesQuery(options); + }, + parse: async ( + options: HostRulesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.rule_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const hostRulesEdges: HostRulesEdges[] = formatHostRulesData( + getOr([], 'aggregations.rule_name.buckets', response.rawResponse) + ); + + const edges = hostRulesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostRulesQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts new file mode 100644 index 0000000000000..4c116104b3e14 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, HostRulesRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildHostRulesQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: HostRulesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_name: { + terms: { + field: 'signal.rule.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_type: { + terms: { + field: 'signal.rule.type', + }, + }, + }, + }, + rule_count: { + cardinality: { + field: 'signal.rule.name', + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts new file mode 100644 index 0000000000000..b20cf4582c824 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { + HostTacticsHit, + HostTacticsEdges, + HostTacticsFields, + HostTechniqueHit, +} from '../../../../../../common'; + +export const formatHostTacticsData = (buckets: HostTacticsHit[]): HostTacticsEdges[] => + buckets.reduce((acc: HostTacticsEdges[], bucket) => { + return [ + ...acc, + ...getOr([], 'technique.buckets', bucket).map((t: HostTechniqueHit) => ({ + node: { + _id: bucket.key + t.key, + [HostTacticsFields.hits]: t.doc_count, + [HostTacticsFields.riskScore]: getOr(0, 'risk_score.value', t), + [HostTacticsFields.tactic]: bucket.key, + [HostTacticsFields.technique]: t.key, + }, + cursor: { + value: bucket.key + t.key, + tiebreaker: null, + }, + })), + ]; + }, []); +// buckets.map((bucket) => ({ +// node: { +// _id: bucket.key, +// [HostTacticsFields.hits]: bucket.doc_count, +// [HostTacticsFields.riskScore]: getOr(0, 'risk_score.value', bucket), +// [HostTacticsFields.tactic]: bucket.key, +// [HostTacticsFields.technique]: getOr(0, 'technique.buckets[0].key', bucket), +// }, +// cursor: { +// value: bucket.key, +// tiebreaker: null, +// }, +// })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts new file mode 100644 index 0000000000000..0ba8cbef1d144 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + HostTacticsEdges, + HostTacticsRequestOptions, + HostTacticsStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildHostTacticsQuery } from './query.host_tactics.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatHostTacticsData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const hostTactics: SecuritySolutionFactory = { + buildDsl: (options: HostTacticsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildHostTacticsQuery(options); + }, + parse: async ( + options: HostTacticsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.tactic_count.value', response.rawResponse); + const techniqueCount = getOr(0, 'aggregations.technique_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const hostTacticsEdges: HostTacticsEdges[] = formatHostTacticsData( + getOr([], 'aggregations.tactic.buckets', response.rawResponse) + ); + const edges = hostTacticsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostTacticsQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + techniqueCount, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts new file mode 100644 index 0000000000000..ec1afe247011b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { HostTacticsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildHostTacticsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: HostTacticsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + tactic: { + terms: { + field: 'signal.rule.threat.tactic.name', + }, + aggs: { + technique: { + terms: { + field: 'signal.rule.threat.technique.name', + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + }, + }, + }, + }, + tactic_count: { + cardinality: { + field: 'signal.rule.threat.tactic.name', + }, + }, + technique_count: { + cardinality: { + field: 'signal.rule.threat.technique.name', + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts new file mode 100644 index 0000000000000..90db2ec63260a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FactoryQueryTypes, + UebaQueries, +} from '../../../../../common/search_strategy/security_solution'; +import { SecuritySolutionFactory } from '../types'; +import { hostRules } from './host_rules'; +import { hostTactics } from './host_tactics'; +import { riskScore } from './risk_score'; +import { userRules } from './user_rules'; + +export const uebaFactory: Record> = { + [UebaQueries.hostRules]: hostRules, + [UebaQueries.hostTactics]: hostTactics, + [UebaQueries.riskScore]: riskScore, + [UebaQueries.userRules]: userRules, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts new file mode 100644 index 0000000000000..ace2faf819877 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { RiskScoreHit, RiskScoreEdges } from '../../../../../../common'; + +export const formatRiskScoreData = (buckets: RiskScoreHit[]): RiskScoreEdges[] => + buckets.map((bucket) => ({ + node: { + _id: bucket.key, + host_name: bucket.key, + risk_score: getOr(0, 'risk_score.value', bucket), + risk_keyword: getOr(0, 'risk_keyword.buckets[0].key', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts new file mode 100644 index 0000000000000..6b3a956c9c1b7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + RiskScoreEdges, + RiskScoreRequestOptions, + RiskScoreStrategyResponse, + UebaQueries, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildRiskScoreQuery } from './query.risk_score.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatRiskScoreData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const riskScore: SecuritySolutionFactory = { + buildDsl: (options: RiskScoreRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildRiskScoreQuery(options); + }, + parse: async ( + options: RiskScoreRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const riskScoreEdges: RiskScoreEdges[] = formatRiskScoreData( + getOr([], 'aggregations.host_data.buckets', response.rawResponse) + ); + + const edges = riskScoreEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildRiskScoreQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts new file mode 100644 index 0000000000000..79c50d84e3c92 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, RiskScoreRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildRiskScoreQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + pagination: { querySize }, + sort, + timerange: { from, to }, +}: RiskScoreRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + host_data: { + terms: { + field: 'host.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'risk_score', + }, + }, + risk_keyword: { + terms: { + field: 'risk.keyword', + }, + }, + }, + }, + host_count: { + cardinality: { + field: 'host.name', + }, + }, + }, + query: { bool: { filter } }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts new file mode 100644 index 0000000000000..c0f38af37c1f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { UserRulesHit, UserRulesFields, UserRulesByUser } from '../../../../../../common'; +import { formatHostRulesData } from '../host_rules/helpers'; + +export const formatUserRulesData = (buckets: UserRulesHit[]): UserRulesByUser[] => + buckets.map((user) => ({ + _id: user.key, + [UserRulesFields.userName]: user.key, + [UserRulesFields.riskScore]: getOr(0, 'risk_score.value', user), + [UserRulesFields.ruleCount]: getOr(0, 'rule_count.value', user), + [UserRulesFields.rules]: formatHostRulesData(getOr([], 'rule_name.buckets', user)), + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts new file mode 100644 index 0000000000000..aa525f2c5b741 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { SecuritySolutionFactory } from '../../types'; +import { + UebaQueries, + UserRulesByUser, + UserRulesFields, + UserRulesRequestOptions, + UserRulesStrategyResponse, + UsersRulesHit, +} from '../../../../../../common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { buildUserRulesQuery } from './query.user_rules.dsl'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { formatUserRulesData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +export const userRules: SecuritySolutionFactory = { + buildDsl: (options: UserRulesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildUserRulesQuery(options); + }, + parse: async ( + options: UserRulesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + + const userRulesByUser: UserRulesByUser[] = formatUserRulesData( + getOr([], 'aggregations.user_data.buckets', response.rawResponse) + ); + const inspect = { + dsl: [inspectStringifyObject(buildUserRulesQuery(options))], + }; + return { + ...response, + inspect, + data: userRulesByUser.map((user) => { + const edges = user[UserRulesFields.rules].splice(cursorStart, querySize - cursorStart); + const totalCount = user[UserRulesFields.ruleCount]; + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + [UserRulesFields.userName]: user[UserRulesFields.userName], + [UserRulesFields.riskScore]: user[UserRulesFields.riskScore], + edges, + totalCount, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts new file mode 100644 index 0000000000000..c2242ff00a6c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; +import { Direction, UserRulesRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildUserRulesQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + hostName, + timerange: { from, to }, +}: UserRulesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + return { + allowNoIndices: true, + index: defaultIndex, // can stop getting this from sourcerer and assume default detections index if we want + ignoreUnavailable: true, + track_total_hits: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + user_data: { + terms: { + field: 'user.name', + order: { + risk_score: Direction.desc, + }, + size: 20, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_name: { + terms: { + field: 'signal.rule.name', + order: { + risk_score: Direction.desc, + }, + }, + aggs: { + risk_score: { + sum: { + field: 'signal.rule.risk_score', + }, + }, + rule_type: { + terms: { + field: 'signal.rule.type', + }, + }, + }, + }, + rule_count: { + cardinality: { + field: 'signal.rule.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + must: [ + { + term: { + 'host.name': hostName, + }, + }, + ], + }, + }, + size: 0, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 259c0f2ae2f92..611860929e25e 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -13,6 +13,7 @@ import { APP_ID, DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN, + DEFAULT_INDEX_PATTERN_EXPERIMENTAL, DEFAULT_ANOMALY_SCORE, DEFAULT_APP_TIME_RANGE, DEFAULT_APP_REFRESH_INTERVAL, @@ -88,7 +89,9 @@ export const initUiSettings = ( }), sensitive: true, - value: DEFAULT_INDEX_PATTERN, + value: experimentalFeatures.uebaEnabled + ? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL] + : DEFAULT_INDEX_PATTERN, description: i18n.translate('xpack.securitySolution.uiSettings.defaultIndexDescription', { defaultMessage: '

Comma-delimited list of Elasticsearch indices from which the Security app collects events.

', diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts index f29dc4a3c7450..9a2d884af948f 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts @@ -14,6 +14,7 @@ export enum LastEventIndexKey { hosts = 'hosts', ipDetails = 'ipDetails', network = 'network', + ueba = 'ueba', // TODO: Steph/ueba implement this } export interface LastTimeDetails { diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index c0bc1c305b970..36a5d31bd6904 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -314,6 +314,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes @@ -326,6 +327,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.uebaPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), ]); diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index c8c72e0310958..41f69b9f55d0d 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -45,6 +45,7 @@ export enum TimelineId { detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', + uebaPageExternalAlerts = 'ueba-page-external-alerts', active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts index dd8888aaa15f6..354c682377bac 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts @@ -83,6 +83,7 @@ export const buildLastEventTimeQuery = ({ throw new Error('buildLastEventTimeQuery - no hostName argument provided'); case LastEventIndexKey.hosts: case LastEventIndexKey.network: + case LastEventIndexKey.ueba: return { allowNoIndices: true, index: indicesToQuery[indexKey],