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