From c0ceb06f4b8f67a3ceff1ccf8b8b48b3866f5c0f Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Fri, 23 Jul 2021 09:49:55 -0500
Subject: [PATCH 1/5] [Security Solution] UEBA Spacetime Project (#104973)
Merging with known issues documented here: https://github.com/elastic/kibana/issues/106648
---
.../security_solution/common/constants.ts | 24 +-
.../common/experimental_features.ts | 5 +-
.../security_solution/index.ts | 29 +++
.../security_solution/ueba/common/index.ts | 52 ++++
.../ueba/host_rules/index.ts | 48 ++++
.../ueba/host_tactics/index.ts | 52 ++++
.../security_solution/ueba/index.ts | 19 ++
.../ueba/risk_score/index.ts | 47 ++++
.../ueba/user_rules/index.ts | 78 ++++++
.../common/types/timeline/index.ts | 2 +
.../public/app/deep_links/index.test.ts | 38 ++-
.../public/app/deep_links/index.ts | 36 ++-
.../public/app/home/home_navigations.ts | 8 +
.../security_solution/public/app/index.tsx | 4 +-
.../public/app/translations.ts | 4 +
.../security_solution/public/app/types.ts | 8 +-
.../common/components/header_page/index.tsx | 4 +-
.../components/link_to/redirect_to_ueba.tsx | 24 ++
.../public/common/components/links/index.tsx | 40 ++++
.../navigation/breadcrumbs/index.ts | 24 ++
.../navigation/tab_navigation/index.tsx | 3 +-
.../common/components/navigation/types.ts | 26 +-
.../index.test.tsx | 14 ++
.../index.tsx | 13 +-
.../use_navigation_items.tsx | 4 +-
.../components/paginated_table/index.tsx | 19 +-
.../common/components/url_state/constants.ts | 7 +-
.../common/components/url_state/types.ts | 12 +-
.../common/containers/sourcerer/index.tsx | 15 +-
.../common/hooks/use_experimental_features.ts | 5 +-
.../mock/endpoint/app_context_render.tsx | 2 +-
.../public/common/mock/global_state.ts | 42 +++-
.../public/common/mock/utils.ts | 2 +
.../public/common/store/app/model.ts | 2 +-
.../public/common/store/app/reducer.ts | 7 +
.../public/common/store/reducer.ts | 2 +
.../public/common/store/types.ts | 2 +
.../public/common/utils/route/types.ts | 11 +-
.../timeline_actions/alert_context_menu.tsx | 14 +-
.../detection_engine/rules/details/index.tsx | 20 +-
.../security_solution/public/helpers.ts | 4 +-
.../public/lazy_sub_plugins.tsx | 2 +
.../security_solution/public/plugin.tsx | 38 ++-
.../plugins/security_solution/public/types.ts | 5 +
.../components/host_rules_table/columns.tsx | 145 +++++++++++
.../components/host_rules_table/index.tsx | 173 ++++++++++++++
.../host_rules_table/translations.ts | 33 +++
.../components/host_tactics_table/columns.tsx | 153 ++++++++++++
.../components/host_tactics_table/index.tsx | 161 +++++++++++++
.../host_tactics_table/translations.ts | 33 +++
.../components/risk_score_table/columns.tsx | 79 ++++++
.../components/risk_score_table/index.tsx | 157 ++++++++++++
.../risk_score_table/translations.ts | 29 +++
.../public/ueba/components/translations.ts | 18 ++
.../public/ueba/components/utils.ts | 20 ++
.../ueba/containers/host_rules/index.tsx | 220 +++++++++++++++++
.../containers/host_rules/translations.ts | 22 ++
.../ueba/containers/host_tactics/index.tsx | 225 ++++++++++++++++++
.../containers/host_tactics/translations.ts | 22 ++
.../ueba/containers/risk_score/index.tsx | 216 +++++++++++++++++
.../containers/risk_score/translations.ts | 22 ++
.../ueba/containers/user_rules/index.tsx | 209 ++++++++++++++++
.../containers/user_rules/translations.ts | 22 ++
.../security_solution/public/ueba/index.ts | 30 +++
.../ueba/pages/details/details_tabs.tsx | 95 ++++++++
.../public/ueba/pages/details/helpers.ts | 50 ++++
.../public/ueba/pages/details/index.tsx | 150 ++++++++++++
.../public/ueba/pages/details/nav_tabs.tsx | 37 +++
.../public/ueba/pages/details/types.ts | 65 +++++
.../public/ueba/pages/details/utils.ts | 71 ++++++
.../public/ueba/pages/display.tsx | 14 ++
.../public/ueba/pages/index.tsx | 65 +++++
.../public/ueba/pages/nav_tabs.tsx | 22 ++
.../navigation/host_rules_query_tab_body.tsx | 64 +++++
.../host_tactics_query_tab_body.tsx | 63 +++++
.../public/ueba/pages/navigation/index.ts | 11 +
.../navigation/risk_score_query_tab_body.tsx | 52 ++++
.../public/ueba/pages/navigation/types.ts | 39 +++
.../navigation/user_rules_query_tab_body.tsx | 70 ++++++
.../public/ueba/pages/translations.ts | 27 +++
.../public/ueba/pages/types.ts | 29 +++
.../public/ueba/pages/ueba.tsx | 184 ++++++++++++++
.../public/ueba/pages/ueba_tabs.tsx | 82 +++++++
.../security_solution/public/ueba/routes.tsx | 26 ++
.../public/ueba/store/actions.ts | 35 +++
.../public/ueba/store/helpers.ts | 45 ++++
.../public/ueba/store/index.ts | 22 ++
.../public/ueba/store/model.ts | 78 ++++++
.../public/ueba/store/reducer.ts | 136 +++++++++++
.../public/ueba/store/selectors.ts | 27 +++
.../signals/executors/eql.test.ts | 2 +
.../detection_engine/signals/executors/eql.ts | 10 +-
.../signals/executors/query.ts | 10 +-
.../signals/executors/threat_match.ts | 10 +-
.../signals/executors/threshold.test.ts | 2 +
.../signals/executors/threshold.ts | 10 +-
.../signals/get_input_output_index.test.ts | 64 ++++-
.../signals/get_input_output_index.ts | 33 ++-
.../signals/signal_rule_alert_type.test.ts | 2 +
.../signals/signal_rule_alert_type.ts | 14 +-
.../security_solution/server/plugin.ts | 1 +
.../factory/hosts/details/index.test.tsx | 1 +
.../security_solution/factory/index.ts | 2 +
.../factory/ueba/host_rules/helpers.ts | 24 ++
.../factory/ueba/host_rules/index.ts | 59 +++++
.../ueba/host_rules/query.host_rules.dsl.ts | 86 +++++++
.../factory/ueba/host_tactics/helpers.ts | 47 ++++
.../factory/ueba/host_tactics/index.ts | 59 +++++
.../host_tactics/query.host_tactics.dsl.ts | 90 +++++++
.../security_solution/factory/ueba/index.ts | 23 ++
.../factory/ueba/risk_score/helpers.ts | 23 ++
.../factory/ueba/risk_score/index.ts | 59 +++++
.../ueba/risk_score/query.risk_score.dsl.ts | 71 ++++++
.../factory/ueba/user_rules/helpers.ts | 19 ++
.../factory/ueba/user_rules/index.ts | 67 ++++++
.../ueba/user_rules/query.user_rules.dsl.ts | 97 ++++++++
.../security_solution/server/ui_settings.ts | 5 +-
.../timeline/events/last_event_time/index.ts | 1 +
.../timelines/common/types/timeline/index.ts | 2 +
.../timelines/public/store/t_grid/types.ts | 1 +
.../query.events_last_event_time.dsl.ts | 1 +
121 files changed, 5143 insertions(+), 117 deletions(-)
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/common/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_rules/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/host_tactics/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/risk_score/index.ts
create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/ueba/user_rules/index.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_ueba.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_rules_table/columns.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_rules_table/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_rules_table/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/columns.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/host_tactics_table/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/risk_score_table/columns.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/risk_score_table/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/risk_score_table/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/components/utils.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/host_rules/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/host_rules/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/host_tactics/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/host_tactics/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/risk_score/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/risk_score/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/user_rules/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/containers/user_rules/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/index.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/details_tabs.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/helpers.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/nav_tabs.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/types.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/details/utils.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/display.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/index.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/nav_tabs.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/host_rules_query_tab_body.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/host_tactics_query_tab_body.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/index.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/risk_score_query_tab_body.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/types.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/navigation/user_rules_query_tab_body.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/types.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/ueba.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/pages/ueba_tabs.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/routes.tsx
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/actions.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/helpers.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/index.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/model.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/reducer.ts
create mode 100644 x-pack/plugins/security_solution/public/ueba/store/selectors.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_rules/query.host_rules.dsl.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/host_tactics/query.host_tactics.dsl.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/risk_score/query.risk_score.dsl.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/helpers.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/index.ts
create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/ueba/user_rules/query.user_rules.dsl.ts
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 27d4a5c9fd399..48a23a967059e 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -62,20 +62,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';
@@ -86,6 +87,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`;
@@ -100,6 +102,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}`;
@@ -119,6 +122,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 f5cec592c7abf..871f1a01e3de0 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 66f62ad3ebeab..8770e59e0c178 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
@@ -86,7 +86,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';
@@ -227,6 +231,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 {
@@ -348,7 +355,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);
@@ -732,7 +746,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 9d2e918d4f274..4a346581b7767 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -290,6 +290,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 6d3d8ac3c55aa..0fc6ce78ee982 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
@@ -82,6 +82,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],
From e45d25dde00ab5c1882522a78502ddf7f77c7d7f Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Sun, 25 Jul 2021 15:59:24 +0200
Subject: [PATCH 2/5] Fix vis. search filter being overridden in dashboard
(#106399)
* fix vis search filter context being overridden in dashboard
* Revert "fix vis search filter context being overridden in dashboard"
This reverts commit ead7ef6ed34d9a3acbfe4f7bdeae1063fc7ce8c0.
* updated filtering order in kibana context
* use buildFilter
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../search/expressions/kibana_context.test.ts | 264 ++++++++++++++++++
.../search/expressions/kibana_context.ts | 2 +-
2 files changed, 265 insertions(+), 1 deletion(-)
create mode 100644 src/plugins/data/common/search/expressions/kibana_context.test.ts
diff --git a/src/plugins/data/common/search/expressions/kibana_context.test.ts b/src/plugins/data/common/search/expressions/kibana_context.test.ts
new file mode 100644
index 0000000000000..77d89792b63c3
--- /dev/null
+++ b/src/plugins/data/common/search/expressions/kibana_context.test.ts
@@ -0,0 +1,264 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { FilterStateStore, buildFilter, FILTERS } from '@kbn/es-query';
+import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
+import type { ExecutionContext } from 'src/plugins/expressions/common';
+import { KibanaContext } from './kibana_context_type';
+
+import {
+ getKibanaContextFn,
+ ExpressionFunctionKibanaContext,
+ KibanaContextStartDependencies,
+} from './kibana_context';
+
+type StartServicesMock = DeeplyMockedKeys;
+
+const createExecutionContextMock = (): DeeplyMockedKeys => ({
+ abortSignal: {} as any,
+ getExecutionContext: jest.fn(),
+ getSearchContext: jest.fn(),
+ getSearchSessionId: jest.fn(),
+ inspectorAdapters: jest.fn(),
+ types: {},
+ variables: {},
+ getKibanaRequest: jest.fn(),
+});
+
+const emptyArgs = { q: null, timeRange: null, savedSearchId: null };
+
+describe('kibanaContextFn', () => {
+ let kibanaContextFn: ExpressionFunctionKibanaContext;
+ let startServicesMock: StartServicesMock;
+
+ const getStartServicesMock = (): Promise => Promise.resolve(startServicesMock);
+
+ beforeEach(async () => {
+ kibanaContextFn = getKibanaContextFn(getStartServicesMock);
+ startServicesMock = {
+ savedObjectsClient: {
+ create: jest.fn(),
+ delete: jest.fn(),
+ find: jest.fn(),
+ get: jest.fn(),
+ update: jest.fn(),
+ },
+ };
+ });
+
+ it('merges and deduplicates queries from different sources', async () => {
+ const { fn } = kibanaContextFn;
+ startServicesMock.savedObjectsClient.get.mockResolvedValue({
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({
+ query: [
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ test: 'something1',
+ },
+ },
+ },
+ ],
+ }),
+ },
+ },
+ } as any);
+ const args = {
+ ...emptyArgs,
+ q: {
+ type: 'kibana_query' as 'kibana_query',
+ language: 'test',
+ query: {
+ type: 'test',
+ match_phrase: {
+ test: 'something2',
+ },
+ },
+ },
+ savedSearchId: 'test',
+ };
+ const input: KibanaContext = {
+ type: 'kibana_context',
+ query: [
+ {
+ language: 'kuery',
+ query: [
+ // TODO: Is it expected that if we pass in an array that the values in the array are not deduplicated?
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ test: 'something3',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ timeRange: {
+ from: 'now-24h',
+ to: 'now',
+ },
+ };
+
+ const { query } = await fn(input, args, createExecutionContextMock());
+
+ expect(query).toEqual([
+ {
+ language: 'kuery',
+ query: [
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ test: 'something3',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'kibana_query',
+ language: 'test',
+ query: {
+ type: 'test',
+ match_phrase: {
+ test: 'something2',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ DUPLICATE: 'DUPLICATE',
+ },
+ },
+ },
+ {
+ language: 'kuery',
+ query: {
+ match_phrase: {
+ test: 'something1',
+ },
+ },
+ },
+ ]);
+ });
+
+ it('deduplicates duplicated filters and keeps the first enabled filter', async () => {
+ const { fn } = kibanaContextFn;
+ const filter1 = buildFilter(
+ { fields: [] },
+ { name: 'test', type: 'test' },
+ FILTERS.PHRASE,
+ false,
+ true,
+ {
+ query: 'JetBeats',
+ },
+ null,
+ FilterStateStore.APP_STATE
+ );
+ const filter2 = buildFilter(
+ { fields: [] },
+ { name: 'test', type: 'test' },
+ FILTERS.PHRASE,
+ false,
+ false,
+ {
+ query: 'JetBeats',
+ },
+ null,
+ FilterStateStore.APP_STATE
+ );
+
+ const filter3 = buildFilter(
+ { fields: [] },
+ { name: 'test', type: 'test' },
+ FILTERS.PHRASE,
+ false,
+ false,
+ {
+ query: 'JetBeats',
+ },
+ null,
+ FilterStateStore.APP_STATE
+ );
+
+ const input: KibanaContext = {
+ type: 'kibana_context',
+ query: [
+ {
+ language: 'kuery',
+ query: '',
+ },
+ ],
+ filters: [filter1, filter2, filter3],
+ timeRange: {
+ from: 'now-24h',
+ to: 'now',
+ },
+ };
+
+ const { filters } = await fn(input, emptyArgs, createExecutionContextMock());
+ expect(filters!.length).toBe(1);
+ expect(filters![0]).toBe(filter2);
+ });
+});
diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts
index 9c1c78604ea83..8112777b9b0f3 100644
--- a/src/plugins/data/common/search/expressions/kibana_context.ts
+++ b/src/plugins/data/common/search/expressions/kibana_context.ts
@@ -146,7 +146,7 @@ export const getKibanaContextFn = (
return {
type: 'kibana_context',
query: queries,
- filters: uniqFilters(filters).filter((f: any) => !f.meta?.disabled),
+ filters: uniqFilters(filters.filter((f: any) => !f.meta?.disabled)),
timeRange,
};
},
From f7a308859fab15cd72622900f70bfb9ed9cd8b95 Mon Sep 17 00:00:00 2001
From: Byron Hulcher
Date: Sun, 25 Jul 2021 10:53:17 -0400
Subject: [PATCH 3/5] [App Search] New Add Domain Flyout for Crawler (#106102)
* Added /api/app_search/engines/{name}/crawler/domains to Crawler routes
* New AddDomainLogic
* New AddDomainFormSubmitButton component
* New AddDomainFormErrors component
* New AddDomainForm component
* New AddDomainFlyout component
* Add AddDomainFlyout to CrawlerOverview
* Use exact path for CrawlerOverview in CrawlerRouter
* Clean-up AddDomainFlyout
* Clean-up AddDomainForm
* Clean-up AddDomainFormSubmitButton
* Extract getErrorsFromHttpResponse from flashAPIErrors
* Clean-up AddDomainLogic
* Remove unused imports
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../add_domain/add_domain_flyout.test.tsx | 69 ++++
.../add_domain/add_domain_flyout.tsx | 98 ++++++
.../add_domain/add_domain_form.test.tsx | 122 +++++++
.../components/add_domain/add_domain_form.tsx | 105 ++++++
.../add_domain_form_errors.test.tsx | 41 +++
.../add_domain/add_domain_form_errors.tsx | 40 +++
.../add_domain_form_submit_button.test.tsx | 59 ++++
.../add_domain_form_submit_button.tsx | 30 ++
.../add_domain/add_domain_logic.test.ts | 300 ++++++++++++++++++
.../components/add_domain/add_domain_logic.ts | 166 ++++++++++
.../components/add_domain/utils.test.ts | 23 ++
.../crawler/components/add_domain/utils.ts | 21 ++
.../crawler/crawler_overview.test.tsx | 3 +-
.../components/crawler/crawler_overview.tsx | 20 ++
.../components/crawler/crawler_router.tsx | 4 +-
.../flash_messages/handle_api_errors.test.ts | 25 +-
.../flash_messages/handle_api_errors.ts | 21 +-
.../server/routes/app_search/crawler.test.ts | 56 ++++
.../server/routes/app_search/crawler.ts | 25 ++
19 files changed, 1217 insertions(+), 11 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx
new file mode 100644
index 0000000000000..bdedc9357fa0e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiButton, EuiButtonEmpty, EuiFlyout, EuiFlyoutBody } from '@elastic/eui';
+
+import { AddDomainFlyout } from './add_domain_flyout';
+import { AddDomainForm } from './add_domain_form';
+import { AddDomainFormErrors } from './add_domain_form_errors';
+import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
+
+describe('AddDomainFlyout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('is hidden by default', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiFlyout)).toHaveLength(0);
+ });
+
+ it('displays the flyout when the button is pressed', () => {
+ const wrapper = shallow();
+
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(wrapper.find(EuiFlyout)).toHaveLength(1);
+ });
+
+ describe('flyout', () => {
+ let wrapper: ShallowWrapper;
+
+ beforeEach(() => {
+ wrapper = shallow();
+
+ wrapper.find(EuiButton).simulate('click');
+ });
+
+ it('displays form errors', () => {
+ expect(wrapper.find(EuiFlyoutBody).dive().find(AddDomainFormErrors)).toHaveLength(1);
+ });
+
+ it('contains a form to add domains', () => {
+ expect(wrapper.find(AddDomainForm)).toHaveLength(1);
+ });
+
+ it('contains a cancel buttonn', () => {
+ wrapper.find(EuiButtonEmpty).simulate('click');
+ expect(wrapper.find(EuiFlyout)).toHaveLength(0);
+ });
+
+ it('contains a submit button', () => {
+ expect(wrapper.find(AddDomainFormSubmitButton)).toHaveLength(1);
+ });
+
+ it('hides the flyout on close', () => {
+ wrapper.find(EuiFlyout).simulate('close');
+
+ expect(wrapper.find(EuiFlyout)).toHaveLength(0);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx
new file mode 100644
index 0000000000000..f8511d1e2ef14
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_flyout.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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, { useState } from 'react';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiPortal,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants';
+
+import { AddDomainForm } from './add_domain_form';
+import { AddDomainFormErrors } from './add_domain_form_errors';
+import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
+
+export const AddDomainFlyout: React.FC = () => {
+ const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
+
+ return (
+ <>
+ setIsFlyoutVisible(true)}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.openButtonLabel',
+ {
+ defaultMessage: 'Add domain',
+ }
+ )}
+
+
+ {isFlyoutVisible && (
+
+ setIsFlyoutVisible(false)}>
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.title',
+ {
+ defaultMessage: 'Add a new domain',
+ }
+ )}
+
+
+
+ }>
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.description',
+ {
+ defaultMessage:
+ 'You can add multiple domains to this engine\'s web crawler. Add another domain here and modify the entry points and crawl rules from the "Manage" page.',
+ }
+ )}
+
+
+
+
+
+
+
+
+ setIsFlyoutVisible(false)}>
+ {CANCEL_BUTTON_LABEL}
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx
new file mode 100644
index 0000000000000..6c869d9371f6f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.test.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiButton, EuiFieldText, EuiForm } from '@elastic/eui';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { rerender } from '../../../../../test_helpers';
+
+import { AddDomainForm } from './add_domain_form';
+
+const MOCK_VALUES = {
+ addDomainFormInputValue: 'https://',
+ entryPointValue: '/',
+};
+
+const MOCK_ACTIONS = {
+ setAddDomainFormInputValue: jest.fn(),
+ validateDomain: jest.fn(),
+};
+
+describe('AddDomainForm', () => {
+ let wrapper: ShallowWrapper;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(MOCK_ACTIONS);
+ setMockValues(MOCK_VALUES);
+ wrapper = shallow();
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(EuiForm)).toHaveLength(1);
+ });
+
+ it('contains a submit button', () => {
+ expect(wrapper.find(EuiButton).prop('type')).toEqual('submit');
+ });
+
+ it('validates domain on submit', () => {
+ wrapper.find(EuiForm).simulate('submit', { preventDefault: jest.fn() });
+
+ expect(MOCK_ACTIONS.validateDomain).toHaveBeenCalledTimes(1);
+ });
+
+ describe('url field', () => {
+ it('uses the value from the logic', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ addDomainFormInputValue: 'test value',
+ });
+
+ rerender(wrapper);
+
+ expect(wrapper.find(EuiFieldText).prop('value')).toEqual('test value');
+ });
+
+ it('sets the value in the logic on change', () => {
+ wrapper.find(EuiFieldText).simulate('change', { target: { value: 'test value' } });
+
+ expect(MOCK_ACTIONS.setAddDomainFormInputValue).toHaveBeenCalledWith('test value');
+ });
+ });
+
+ describe('validate domain button', () => {
+ it('is enabled when the input has a value', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ addDomainFormInputValue: 'https://elastic.co',
+ });
+
+ rerender(wrapper);
+
+ expect(wrapper.find(EuiButton).prop('disabled')).toEqual(false);
+ });
+
+ it('is disabled when the input value is empty', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ addDomainFormInputValue: '',
+ });
+
+ rerender(wrapper);
+
+ expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true);
+ });
+ });
+
+ describe('entry point indicator', () => {
+ it('is hidden when the entry point is /', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ entryPointValue: '/',
+ });
+
+ rerender(wrapper);
+
+ expect(wrapper.find(FormattedMessage)).toHaveLength(0);
+ });
+
+ it('displays the entry point otherwise', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ entryPointValue: '/guide',
+ });
+
+ rerender(wrapper);
+
+ expect(wrapper.find(FormattedMessage)).toHaveLength(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx
new file mode 100644
index 0000000000000..de6a33403c2ed
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 { useActions, useValues } from 'kea';
+
+import {
+ EuiButton,
+ EuiCode,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiFormRow,
+ EuiFieldText,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { AddDomainLogic } from './add_domain_logic';
+
+export const AddDomainForm: React.FC = () => {
+ const { setAddDomainFormInputValue, validateDomain } = useActions(AddDomainLogic);
+
+ const { addDomainFormInputValue, entryPointValue } = useValues(AddDomainLogic);
+
+ return (
+ <>
+ {
+ event.preventDefault();
+ validateDomain();
+ }}
+ component="form"
+ >
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.urlHelpText',
+ {
+ defaultMessage: 'Domain URLs require a protocol and cannot contain any paths.',
+ }
+ )}
+
+ }
+ >
+
+
+ setAddDomainFormInputValue(e.target.value)}
+ fullWidth
+ />
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.validateButtonLabel',
+ {
+ defaultMessage: 'Validate Domain',
+ }
+ )}
+
+
+
+
+
+ {entryPointValue !== '/' && (
+ <>
+
+
+
+
+ {entryPointValue},
+ }}
+ />
+
+
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx
new file mode 100644
index 0000000000000..d2c3ac37d58fa
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { setMockValues } from '../../../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AddDomainFormErrors } from './add_domain_form_errors';
+
+describe('AddDomainFormErrors', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('is empty when there are no errors', () => {
+ setMockValues({
+ errors: [],
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+
+ it('displays all the errors from the logic', () => {
+ setMockValues({
+ errors: ['first error', 'second error'],
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.find('p')).toHaveLength(2);
+ expect(wrapper.find('p').first().text()).toContain('first error');
+ expect(wrapper.find('p').last().text()).toContain('second error');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx
new file mode 100644
index 0000000000000..890657d4c235a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_errors.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { useValues } from 'kea';
+
+import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { AddDomainLogic } from './add_domain_logic';
+
+export const AddDomainFormErrors: React.FC = () => {
+ const { errors } = useValues(AddDomainLogic);
+
+ if (errors.length > 0) {
+ return (
+
+ {errors.map((message, index) => (
+ {message}
+ ))}
+
+ );
+ }
+
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx
new file mode 100644
index 0000000000000..a01d8c55bc87c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.test.tsx
@@ -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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton } from '@elastic/eui';
+
+import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
+
+describe('AddDomainFormSubmitButton', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('is disabled when the domain has not been validated', () => {
+ setMockValues({
+ hasValidationCompleted: false,
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.prop('disabled')).toBe(true);
+ });
+
+ it('is enabled when the domain has been validated', () => {
+ setMockValues({
+ hasValidationCompleted: true,
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.prop('disabled')).toBe(false);
+ });
+
+ it('submits the domain on click', () => {
+ const submitNewDomain = jest.fn();
+
+ setMockActions({
+ submitNewDomain,
+ });
+ setMockValues({
+ hasValidationCompleted: true,
+ });
+
+ const wrapper = shallow();
+
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(submitNewDomain).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx
new file mode 100644
index 0000000000000..dbf5f86ca70fc
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_form_submit_button.tsx
@@ -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 React from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import { EuiButton } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { AddDomainLogic } from './add_domain_logic';
+
+export const AddDomainFormSubmitButton: React.FC = () => {
+ const { submitNewDomain } = useActions(AddDomainLogic);
+
+ const { hasValidationCompleted } = useValues(AddDomainLogic);
+
+ return (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.addDomainForm.submitButtonLabel', {
+ defaultMessage: 'Add domain',
+ })}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts
new file mode 100644
index 0000000000000..3072796b7194f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts
@@ -0,0 +1,300 @@
+/*
+ * 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 {
+ LogicMounter,
+ mockFlashMessageHelpers,
+ mockHttpValues,
+ mockKibanaValues,
+} from '../../../../../__mocks__/kea_logic';
+import '../../../../__mocks__/engine_logic.mock';
+
+jest.mock('../../crawler_overview_logic', () => ({
+ CrawlerOverviewLogic: {
+ actions: {
+ onReceiveCrawlerData: jest.fn(),
+ },
+ },
+}));
+
+import { nextTick } from '@kbn/test/jest';
+
+import { CrawlerOverviewLogic } from '../../crawler_overview_logic';
+import { CrawlerDomain } from '../../types';
+
+import { AddDomainLogic, AddDomainLogicValues } from './add_domain_logic';
+
+const DEFAULT_VALUES: AddDomainLogicValues = {
+ addDomainFormInputValue: 'https://',
+ allowSubmit: false,
+ entryPointValue: '/',
+ hasValidationCompleted: false,
+ errors: [],
+};
+
+describe('AddDomainLogic', () => {
+ const { mount } = new LogicMounter(AddDomainLogic);
+ const { flashSuccessToast } = mockFlashMessageHelpers;
+ const { http } = mockHttpValues;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('has default values', () => {
+ mount();
+ expect(AddDomainLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('actions', () => {
+ describe('clearDomainFormInputValue', () => {
+ beforeAll(() => {
+ mount({
+ addDomainFormInputValue: 'http://elastic.co',
+ entryPointValue: '/foo',
+ hasValidationCompleted: true,
+ errors: ['first error', 'second error'],
+ });
+
+ AddDomainLogic.actions.clearDomainFormInputValue();
+ });
+
+ it('should clear the input value', () => {
+ expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://');
+ });
+
+ it('should clear the entry point value', () => {
+ expect(AddDomainLogic.values.entryPointValue).toEqual('/');
+ });
+
+ it('should reset validation completion', () => {
+ expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false);
+ });
+
+ it('should clear errors', () => {
+ expect(AddDomainLogic.values.errors).toEqual([]);
+ });
+ });
+
+ describe('onSubmitNewDomainError', () => {
+ it('should set errors', () => {
+ mount();
+
+ AddDomainLogic.actions.onSubmitNewDomainError(['first error', 'second error']);
+
+ expect(AddDomainLogic.values.errors).toEqual(['first error', 'second error']);
+ });
+ });
+
+ describe('onValidateDomain', () => {
+ beforeAll(() => {
+ mount({
+ addDomainFormInputValue: 'https://elastic.co',
+ entryPointValue: '/customers',
+ hasValidationCompleted: true,
+ errors: ['first error', 'second error'],
+ });
+
+ AddDomainLogic.actions.onValidateDomain('https://swiftype.com', '/site-search');
+ });
+
+ it('should set the input value', () => {
+ expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://swiftype.com');
+ });
+
+ it('should set the entry point value', () => {
+ expect(AddDomainLogic.values.entryPointValue).toEqual('/site-search');
+ });
+
+ it('should flag validation as being completed', () => {
+ expect(AddDomainLogic.values.hasValidationCompleted).toEqual(true);
+ });
+
+ it('should clear errors', () => {
+ expect(AddDomainLogic.values.errors).toEqual([]);
+ });
+ });
+
+ describe('setAddDomainFormInputValue', () => {
+ beforeAll(() => {
+ mount({
+ addDomainFormInputValue: 'https://elastic.co',
+ entryPointValue: '/customers',
+ hasValidationCompleted: true,
+ errors: ['first error', 'second error'],
+ });
+
+ AddDomainLogic.actions.setAddDomainFormInputValue('https://swiftype.com/site-search');
+ });
+
+ it('should set the input value', () => {
+ expect(AddDomainLogic.values.addDomainFormInputValue).toEqual(
+ 'https://swiftype.com/site-search'
+ );
+ });
+
+ it('should clear the entry point value', () => {
+ expect(AddDomainLogic.values.entryPointValue).toEqual('/');
+ });
+
+ it('should reset validation completion', () => {
+ expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false);
+ });
+
+ it('should clear errors', () => {
+ expect(AddDomainLogic.values.errors).toEqual([]);
+ });
+ });
+
+ describe('submitNewDomain', () => {
+ it('should clear errors', () => {
+ expect(AddDomainLogic.values.errors).toEqual([]);
+ });
+ });
+ });
+
+ describe('listeners', () => {
+ describe('onSubmitNewDomainSuccess', () => {
+ it('should flash a success toast', () => {
+ const { navigateToUrl } = mockKibanaValues;
+ mount();
+
+ AddDomainLogic.actions.onSubmitNewDomainSuccess({ id: 'test-domain' } as CrawlerDomain);
+
+ expect(flashSuccessToast).toHaveBeenCalled();
+ expect(navigateToUrl).toHaveBeenCalledWith(
+ '/engines/some-engine/crawler/domains/test-domain'
+ );
+ });
+ });
+
+ describe('submitNewDomain', () => {
+ it('calls the domains endpoint with a JSON formatted body', async () => {
+ mount({
+ addDomainFormInputValue: 'https://elastic.co',
+ entryPointValue: '/guide',
+ });
+ http.post.mockReturnValueOnce(Promise.resolve({}));
+
+ AddDomainLogic.actions.submitNewDomain();
+ await nextTick();
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/crawler/domains',
+ {
+ query: {
+ respond_with: 'crawler_details',
+ },
+ body: JSON.stringify({
+ name: 'https://elastic.co',
+ entry_points: [{ value: '/guide' }],
+ }),
+ }
+ );
+ });
+
+ describe('on success', () => {
+ beforeEach(() => {
+ mount();
+ });
+
+ it('sets crawler data', async () => {
+ http.post.mockReturnValueOnce(
+ Promise.resolve({
+ domains: [],
+ })
+ );
+
+ AddDomainLogic.actions.submitNewDomain();
+ await nextTick();
+
+ expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({
+ domains: [],
+ });
+ });
+
+ it('calls the success callback with the most recent domain', async () => {
+ http.post.mockReturnValueOnce(
+ Promise.resolve({
+ domains: [
+ {
+ id: '1',
+ name: 'https://elastic.co/guide',
+ },
+ {
+ id: '2',
+ name: 'https://swiftype.co/site-search',
+ },
+ ],
+ })
+ );
+ jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainSuccess');
+ AddDomainLogic.actions.submitNewDomain();
+ await nextTick();
+
+ expect(AddDomainLogic.actions.onSubmitNewDomainSuccess).toHaveBeenCalledWith({
+ id: '2',
+ url: 'https://swiftype.co/site-search',
+ });
+ });
+ });
+
+ describe('on error', () => {
+ beforeEach(() => {
+ mount();
+ jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainError');
+ });
+
+ it('passes error messages to the error callback', async () => {
+ http.post.mockReturnValueOnce(
+ Promise.reject({
+ body: {
+ attributes: {
+ errors: ['first error', 'second error'],
+ },
+ },
+ })
+ );
+
+ AddDomainLogic.actions.submitNewDomain();
+ await nextTick();
+
+ expect(AddDomainLogic.actions.onSubmitNewDomainError).toHaveBeenCalledWith([
+ 'first error',
+ 'second error',
+ ]);
+ });
+ });
+ });
+
+ describe('validateDomain', () => {
+ it('extracts the domain and entrypoint and passes them to the callback ', () => {
+ mount({ addDomainFormInputValue: 'https://swiftype.com/site-search' });
+ jest.spyOn(AddDomainLogic.actions, 'onValidateDomain');
+
+ AddDomainLogic.actions.validateDomain();
+
+ expect(AddDomainLogic.actions.onValidateDomain).toHaveBeenCalledWith(
+ 'https://swiftype.com',
+ '/site-search'
+ );
+ });
+ });
+ });
+
+ describe('selectors', () => {
+ describe('allowSubmit', () => {
+ it('gets set true when validation is completed', () => {
+ mount({ hasValidationCompleted: false });
+ expect(AddDomainLogic.values.allowSubmit).toEqual(false);
+
+ mount({ hasValidationCompleted: true });
+ expect(AddDomainLogic.values.allowSubmit).toEqual(true);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts
new file mode 100644
index 0000000000000..b05b9454fe8f8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts
@@ -0,0 +1,166 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+
+import { i18n } from '@kbn/i18n';
+
+import { flashSuccessToast } from '../../../../../shared/flash_messages';
+import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors';
+
+import { HttpLogic } from '../../../../../shared/http';
+import { KibanaLogic } from '../../../../../shared/kibana';
+import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../../routes';
+import { EngineLogic, generateEnginePath } from '../../../engine';
+
+import { CrawlerOverviewLogic } from '../../crawler_overview_logic';
+import { CrawlerDataFromServer, CrawlerDomain } from '../../types';
+import { crawlerDataServerToClient } from '../../utils';
+
+import { extractDomainAndEntryPointFromUrl } from './utils';
+
+export interface AddDomainLogicValues {
+ addDomainFormInputValue: string;
+ allowSubmit: boolean;
+ hasValidationCompleted: boolean;
+ entryPointValue: string;
+ errors: string[];
+}
+
+export interface AddDomainLogicActions {
+ clearDomainFormInputValue(): void;
+ setAddDomainFormInputValue(newValue: string): string;
+ onSubmitNewDomainError(errors: string[]): { errors: string[] };
+ onSubmitNewDomainSuccess(domain: CrawlerDomain): { domain: CrawlerDomain };
+ onValidateDomain(
+ newValue: string,
+ newEntryPointValue: string
+ ): { newValue: string; newEntryPointValue: string };
+ submitNewDomain(): void;
+ validateDomain(): void;
+}
+
+const DEFAULT_SELECTOR_VALUES = {
+ addDomainFormInputValue: 'https://',
+ entryPointValue: '/',
+};
+
+export const AddDomainLogic = kea>({
+ path: ['enterprise_search', 'app_search', 'crawler', 'add_domain'],
+ actions: () => ({
+ clearDomainFormInputValue: true,
+ setAddDomainFormInputValue: (newValue) => newValue,
+ onSubmitNewDomainSuccess: (domain) => ({ domain }),
+ onSubmitNewDomainError: (errors) => ({ errors }),
+ onValidateDomain: (newValue, newEntryPointValue) => ({
+ newValue,
+ newEntryPointValue,
+ }),
+ submitNewDomain: true,
+ validateDomain: true,
+ }),
+ reducers: () => ({
+ addDomainFormInputValue: [
+ DEFAULT_SELECTOR_VALUES.addDomainFormInputValue,
+ {
+ clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.addDomainFormInputValue,
+ setAddDomainFormInputValue: (_, newValue: string) => newValue,
+ onValidateDomain: (_, { newValue }: { newValue: string }) => newValue,
+ },
+ ],
+ entryPointValue: [
+ DEFAULT_SELECTOR_VALUES.entryPointValue,
+ {
+ clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue,
+ setAddDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue,
+ onValidateDomain: (_, { newEntryPointValue }) => newEntryPointValue,
+ },
+ ],
+ // TODO When 4-step validation is added this will become a selector as
+ // we'll use individual step results to determine whether this is true/false
+ hasValidationCompleted: [
+ false,
+ {
+ clearDomainFormInputValue: () => false,
+ setAddDomainFormInputValue: () => false,
+ onValidateDomain: () => true,
+ },
+ ],
+ errors: [
+ [],
+ {
+ clearDomainFormInputValue: () => [],
+ setAddDomainFormInputValue: () => [],
+ onValidateDomain: () => [],
+ submitNewDomain: () => [],
+ onSubmitNewDomainError: (_, { errors }) => errors,
+ },
+ ],
+ }),
+ selectors: ({ selectors }) => ({
+ // TODO include selectors.blockingFailures once 4-step validation is migrated
+ allowSubmit: [
+ () => [selectors.hasValidationCompleted], // should eventually also contain selectors.hasBlockingFailures when that is added
+ (hasValidationCompleted: boolean) => hasValidationCompleted, // && !hasBlockingFailures
+ ],
+ }),
+ listeners: ({ actions, values }) => ({
+ onSubmitNewDomainSuccess: ({ domain }) => {
+ flashSuccessToast(
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.add.successMessage',
+ {
+ defaultMessage: "Successfully added domain '{domainUrl}'",
+ values: {
+ domainUrl: domain.url,
+ },
+ }
+ )
+ );
+ KibanaLogic.values.navigateToUrl(
+ generateEnginePath(ENGINE_CRAWLER_DOMAIN_PATH, { domainId: domain.id })
+ );
+ },
+ submitNewDomain: async () => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ const requestBody = JSON.stringify({
+ name: values.addDomainFormInputValue.trim(),
+ entry_points: [{ value: values.entryPointValue }],
+ });
+
+ try {
+ const response = await http.post(`/api/app_search/engines/${engineName}/crawler/domains`, {
+ query: {
+ respond_with: 'crawler_details',
+ },
+ body: requestBody,
+ });
+
+ const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer);
+ CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData);
+ const newDomain = crawlerData.domains[crawlerData.domains.length - 1];
+ if (newDomain) {
+ actions.onSubmitNewDomainSuccess(newDomain);
+ }
+ // If there is not a new domain, that means the server responded with a 200 but
+ // didn't actually persist the new domain to our BE, and we take no action
+ } catch (e) {
+ // we surface errors inside the form instead of in flash messages
+ const errorMessages = getErrorsFromHttpResponse(e);
+ actions.onSubmitNewDomainError(errorMessages);
+ }
+ },
+ validateDomain: () => {
+ const { domain, entryPoint } = extractDomainAndEntryPointFromUrl(
+ values.addDomainFormInputValue.trim()
+ );
+ actions.onValidateDomain(domain, entryPoint);
+ },
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.ts
new file mode 100644
index 0000000000000..446545c28ee79
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.test.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 { extractDomainAndEntryPointFromUrl } from './utils';
+
+describe('extractDomainAndEntryPointFromUrl', () => {
+ it('extracts a provided entry point and domain', () => {
+ expect(extractDomainAndEntryPointFromUrl('https://elastic.co/guide')).toEqual({
+ domain: 'https://elastic.co',
+ entryPoint: '/guide',
+ });
+ });
+
+ it('provides a default entry point if there is only a domain', () => {
+ expect(extractDomainAndEntryPointFromUrl('https://elastic.co')).toEqual({
+ domain: 'https://elastic.co',
+ entryPoint: '/',
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts
new file mode 100644
index 0000000000000..7ba67ae61aa2b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/utils.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const extractDomainAndEntryPointFromUrl = (
+ url: string
+): { domain: string; entryPoint: string } => {
+ let domain = url;
+ let entryPoint = '/';
+
+ const pathSlashIndex = url.search(/[^\:\/]\//);
+ if (pathSlashIndex !== -1) {
+ domain = url.substring(0, pathSlashIndex + 1);
+ entryPoint = url.substring(pathSlashIndex + 1);
+ }
+
+ return { domain, entryPoint };
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
index 3804ecfe7c67d..610ad1f571699 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
@@ -13,6 +13,7 @@ import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
+import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
import { DomainsTable } from './components/domains_table';
import { CrawlerOverview } from './crawler_overview';
@@ -44,7 +45,7 @@ describe('CrawlerOverview', () => {
// TODO test for CrawlRequestsTable after it is built in a future PR
- // TODO test for AddDomainForm after it is built in a future PR
+ expect(wrapper.find(AddDomainFlyout)).toHaveLength(1);
// TODO test for empty state after it is built in a future PR
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
index 9e484df35e7a2..0daac399b7b09 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
@@ -9,9 +9,14 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
import { getEngineBreadcrumbs } from '../engine';
import { AppSearchPageTemplate } from '../layout';
+import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
import { DomainsTable } from './components/domains_table';
import { CRAWLER_TITLE } from './constants';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
@@ -31,6 +36,21 @@ export const CrawlerOverview: React.FC = () => {
pageHeader={{ pageTitle: CRAWLER_TITLE }}
isLoading={dataLoading}
>
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.domainsTitle', {
+ defaultMessage: 'Domains',
+ })}
+
+
+
+
+
+
+
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
index a0145cf76908a..c5dd3907c9019 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx
@@ -8,13 +8,15 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
+import { ENGINE_CRAWLER_PATH } from '../../routes';
+
import { CrawlerLanding } from './crawler_landing';
import { CrawlerOverview } from './crawler_overview';
export const CrawlerRouter: React.FC = () => {
return (
-
+
{process.env.NODE_ENV === 'development' ? : }
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts
index b361e796b4f43..47cbef0bfd953 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts
@@ -9,7 +9,7 @@ import '../../__mocks__/kea_logic/kibana_logic.mock';
import { FlashMessagesLogic } from './flash_messages_logic';
-import { flashAPIErrors } from './handle_api_errors';
+import { flashAPIErrors, getErrorsFromHttpResponse } from './handle_api_errors';
describe('flashAPIErrors', () => {
const mockHttpError = {
@@ -68,10 +68,29 @@ describe('flashAPIErrors', () => {
try {
flashAPIErrors(Error('whatever') as any);
} catch (e) {
- expect(e.message).toEqual('whatever');
expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([
- { type: 'error', message: 'An unexpected error occurred' },
+ { type: 'error', message: expect.any(String) },
]);
}
});
});
+
+describe('getErrorsFromHttpResponse', () => {
+ it('should return errors from the response if present', () => {
+ expect(
+ getErrorsFromHttpResponse({
+ body: { attributes: { errors: ['first error', 'second error'] } },
+ } as any)
+ ).toEqual(['first error', 'second error']);
+ });
+
+ it('should return a message from the responnse if no errors', () => {
+ expect(getErrorsFromHttpResponse({ body: { message: 'test message' } } as any)).toEqual([
+ 'test message',
+ ]);
+ });
+
+ it('should return the a default message otherwise', () => {
+ expect(getErrorsFromHttpResponse({} as any)).toEqual([expect.any(String)]);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts
index 1b5dab0839663..7c82dfb971a1d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts
@@ -40,13 +40,22 @@ export const defaultErrorMessage = i18n.translate(
}
);
+export const getErrorsFromHttpResponse = (response: HttpResponse) => {
+ return Array.isArray(response?.body?.attributes?.errors)
+ ? response.body!.attributes.errors
+ : [response?.body?.message || defaultErrorMessage];
+};
+
/**
* Converts API/HTTP errors into user-facing Flash Messages
*/
-export const flashAPIErrors = (error: HttpResponse, { isQueued }: Options = {}) => {
- const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors)
- ? error.body!.attributes.errors.map((message) => ({ type: 'error', message }))
- : [{ type: 'error', message: error?.body?.message || defaultErrorMessage }];
+export const flashAPIErrors = (
+ response: HttpResponse,
+ { isQueued }: Options = {}
+) => {
+ const errorFlashMessages: IFlashMessage[] = getErrorsFromHttpResponse(
+ response
+ ).map((message) => ({ type: 'error', message }));
if (isQueued) {
FlashMessagesLogic.actions.setQueuedMessages(errorFlashMessages);
@@ -56,7 +65,7 @@ export const flashAPIErrors = (error: HttpResponse, { isQueued }:
// If this was a programming error or a failed request (such as a CORS) error,
// we rethrow the error so it shows up in the developer console
- if (!error?.body?.message) {
- throw error;
+ if (!response?.body?.message) {
+ throw response;
}
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
index 06a206017fbd1..fd478e35064c5 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
@@ -43,6 +43,62 @@ describe('crawler routes', () => {
});
});
+ describe('POST /api/app_search/engines/{name}/crawler/domains', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{name}/crawler/domains',
+ });
+
+ registerCrawlerRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:name/crawler/domains',
+ });
+ });
+
+ it('validates correctly with params and body', () => {
+ const request = {
+ params: { name: 'some-engine' },
+ body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('accepts a query param', () => {
+ const request = {
+ params: { name: 'some-engine' },
+ body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
+ query: { respond_with: 'crawler_details' },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without a name param', () => {
+ const request = {
+ params: {},
+ body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
+ };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation without a body', () => {
+ const request = {
+ params: { name: 'some-engine' },
+ body: {},
+ };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
describe('DELETE /api/app_search/engines/{name}/crawler/domains/{id}', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
index 6c8ed7a49c64a..35bfae763bb9a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
@@ -27,6 +27,31 @@ export function registerCrawlerRoutes({
})
);
+ router.post(
+ {
+ path: '/api/app_search/engines/{name}/crawler/domains',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ body: schema.object({
+ name: schema.string(),
+ entry_points: schema.arrayOf(
+ schema.object({
+ value: schema.string(),
+ })
+ ),
+ }),
+ query: schema.object({
+ respond_with: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:name/crawler/domains',
+ })
+ );
+
router.delete(
{
path: '/api/app_search/engines/{name}/crawler/domains/{id}',
From a1134e1bcaa37353a2aacf8098ae7e3f518b1a04 Mon Sep 17 00:00:00 2001
From: Mat Schaffer
Date: Mon, 26 Jul 2021 13:52:51 +0900
Subject: [PATCH 4/5] Note full cli arg for es full featured snapshot (#103045)
---
docs/developer/advanced/running-elasticsearch.asciidoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc
index e5c86fafd1ce7..324d2af2ed3af 100644
--- a/docs/developer/advanced/running-elasticsearch.asciidoc
+++ b/docs/developer/advanced/running-elasticsearch.asciidoc
@@ -25,7 +25,7 @@ See all available options, like how to specify a specific license, with the `--h
yarn es snapshot --help
----
-`trial` will give you access to all capabilities.
+`--license trial` will give you access to all capabilities.
**Keeping data between snapshots**
From 6a50aff2545167b176a31383ef491bc33e1c6409 Mon Sep 17 00:00:00 2001
From: Yaroslav Kuznietsov
Date: Mon, 26 Jul 2021 09:09:37 +0300
Subject: [PATCH 5/5] [Canvas] Register `expression_functions` in
`{expression}/public/plugin.ts`. (#106636)
* Registered `revealImageFunction` in `public/plugin`.
* Registered `shapeFunction` in `public/plugin`.
---
src/plugins/expression_reveal_image/public/plugin.ts | 2 ++
src/plugins/expression_shape/public/plugin.ts | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/plugins/expression_reveal_image/public/plugin.ts b/src/plugins/expression_reveal_image/public/plugin.ts
index 5f6496a25f820..c3522b43ca0ca 100755
--- a/src/plugins/expression_reveal_image/public/plugin.ts
+++ b/src/plugins/expression_reveal_image/public/plugin.ts
@@ -9,6 +9,7 @@
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public';
import { revealImageRenderer } from './expression_renderers';
+import { revealImageFunction } from '../common/expression_functions';
interface SetupDeps {
expressions: ExpressionsSetup;
@@ -30,6 +31,7 @@ export class ExpressionRevealImagePlugin
StartDeps
> {
public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionRevealImagePluginSetup {
+ expressions.registerFunction(revealImageFunction);
expressions.registerRenderer(revealImageRenderer);
}
diff --git a/src/plugins/expression_shape/public/plugin.ts b/src/plugins/expression_shape/public/plugin.ts
index cb28f97acd697..b20f357d52a9b 100755
--- a/src/plugins/expression_shape/public/plugin.ts
+++ b/src/plugins/expression_shape/public/plugin.ts
@@ -9,6 +9,7 @@
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public';
import { shapeRenderer } from './expression_renderers';
+import { shapeFunction } from '../common/expression_functions';
interface SetupDeps {
expressions: ExpressionsSetup;
@@ -24,6 +25,7 @@ export type ExpressionShapePluginStart = void;
export class ExpressionShapePlugin
implements Plugin {
public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionShapePluginSetup {
+ expressions.registerFunction(shapeFunction);
expressions.registerRenderer(shapeRenderer);
}