diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts index 460e8f1fdae3d..adf849b7ae78b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts @@ -35,3 +35,139 @@ export const EntityAnalyticsPrivileges = z.object({ }), }), }); + +export type AfterKeys = z.infer; +export const AfterKeys = z.object({ + host: z.object({}).catchall(z.string()).optional(), + user: z.object({}).catchall(z.string()).optional(), +}); + +/** + * The identifier of the Kibana data view to be used when generating risk scores. + */ +export type DataViewId = z.infer; +export const DataViewId = z.string(); + +/** + * An elasticsearch DSL filter object. Used to filter the risk inputs involved, which implicitly filters the risk scores themselves. + */ +export type Filter = z.infer; +export const Filter = z.object({}); + +/** + * Specifies how many scores will be involved in a given calculation. Note that this value is per `identifier_type`, i.e. a value of 10 will calculate 10 host scores and 10 user scores, if available. To avoid missed data, keep this value consistent while paginating through scores. + */ +export type PageSize = z.infer; +export const PageSize = z.number().default(1000); + +export type KibanaDate = z.infer; +export const KibanaDate = z.string(); + +/** + * Defines the time period on which risk inputs will be filtered. + */ +export type DateRange = z.infer; +export const DateRange = z.object({ + start: KibanaDate, + end: KibanaDate, +}); + +export type IdentifierType = z.infer; +export const IdentifierType = z.enum(['host', 'user']); +export type IdentifierTypeEnum = typeof IdentifierType.enum; +export const IdentifierTypeEnum = IdentifierType.enum; + +/** + * A generic representation of a document contributing to a Risk Score. + */ +export type RiskScoreInput = z.infer; +export const RiskScoreInput = z.object({ + /** + * The unique identifier (`_id`) of the original source document + */ + id: z.string().optional(), + /** + * The unique index (`_index`) of the original source document + */ + index: z.string().optional(), + /** + * The risk category of the risk input document. + */ + category: z.string().optional(), + /** + * A human-readable description of the risk input document. + */ + description: z.string().optional(), + /** + * The weighted risk score of the risk input document. + */ + risk_score: z.number().min(0).max(100).optional(), + /** + * The @timestamp of the risk input document. + */ + timestamp: z.string().optional(), +}); + +export type RiskScore = z.infer; +export const RiskScore = z.object({ + /** + * The time at which the risk score was calculated. + */ + '@timestamp': z.string().datetime(), + /** + * The identifier field defining this risk score. Coupled with `id_value`, uniquely identifies the entity being scored. + */ + id_field: z.string(), + /** + * The identifier value defining this risk score. Coupled with `id_field`, uniquely identifies the entity being scored. + */ + id_value: z.string(), + /** + * Lexical description of the entity's risk. + */ + calculated_level: z.string(), + /** + * The raw numeric value of the given entity's risk score. + */ + calculated_score: z.number(), + /** + * The normalized numeric value of the given entity's risk score. Useful for comparing with other entities. + */ + calculated_score_norm: z.number().min(0).max(100), + /** + * The contribution of Category 1 to the overall risk score (`calculated_score`). Category 1 contains Detection Engine Alerts. + */ + category_1_score: z.number(), + /** + * The number of risk input documents that contributed to the Category 1 score (`category_1_score`). + */ + category_1_count: z.number(), + /** + * A list of the highest-risk documents contributing to this risk score. Useful for investigative purposes. + */ + inputs: z.array(RiskScoreInput), +}); + +/** + * Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1'). + */ +export type RiskScoreWeight = z.infer; +export const RiskScoreWeight = z.object({ + type: z.string(), + value: z.string().optional(), + host: z.number().min(0).max(1).optional(), + user: z.number().min(0).max(1).optional(), +}); + +/** + * A list of weights to be applied to the scoring calculation. + */ +export type RiskScoreWeights = z.infer; +export const RiskScoreWeights = z.array(RiskScoreWeight); + +export type RiskEngineInitStep = z.infer; +export const RiskEngineInitStep = z.object({ + type: z.string(), + success: z.boolean(), + error: z.string().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml index ab8707059f022..b16729ccc38b3 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml @@ -38,3 +38,195 @@ components: required: - has_all_required - privileges + AfterKeys: + type: object + properties: + host: + type: object + additionalProperties: + type: string + user: + type: object + additionalProperties: + type: string + example: + host: + 'host.name': 'example.host' + user: + 'user.name': 'example_user_name' + + DataViewId: + description: The identifier of the Kibana data view to be used when generating risk scores. + example: security-solution-default + type: string + + Filter: + description: An elasticsearch DSL filter object. Used to filter the risk inputs involved, which implicitly filters the risk scores themselves. + type: object + + PageSize: + description: Specifies how many scores will be involved in a given calculation. Note that this value is per `identifier_type`, i.e. a value of 10 will calculate 10 host scores and 10 user scores, if available. To avoid missed data, keep this value consistent while paginating through scores. + default: 1000 + type: number + + KibanaDate: + type: string + example: '2017-07-21T17:32:28Z' + + DateRange: + description: Defines the time period on which risk inputs will be filtered. + type: object + required: + - start + - end + properties: + start: + $ref: '#/components/schemas/KibanaDate' + end: + $ref: '#/components/schemas/KibanaDate' + + IdentifierType: + type: string + enum: + - host + - user + + RiskScoreInput: + description: A generic representation of a document contributing to a Risk Score. + type: object + properties: + id: + type: string + example: 91a93376a507e86cfbf282166275b89f9dbdb1f0be6c8103c6ff2909ca8e1a1c + description: The unique identifier (`_id`) of the original source document + index: + type: string + example: .internal.alerts-security.alerts-default-000001 + description: The unique index (`_index`) of the original source document + category: + type: string + example: category_1 + description: The risk category of the risk input document. + description: + type: string + example: 'Generated from Detection Engine Rule: Malware Prevention Alert' + description: A human-readable description of the risk input document. + risk_score: + type: number + format: double + minimum: 0 + maximum: 100 + description: The weighted risk score of the risk input document. + timestamp: + type: string + example: '2017-07-21T17:32:28Z' + description: The @timestamp of the risk input document. + + RiskScore: + type: object + required: + - '@timestamp' + - id_field + - id_value + - calculated_level + - calculated_score + - calculated_score_norm + - category_1_score + - category_1_count + - inputs + properties: + '@timestamp': + type: string + format: 'date-time' + example: '2017-07-21T17:32:28Z' + description: The time at which the risk score was calculated. + id_field: + type: string + example: 'host.name' + description: The identifier field defining this risk score. Coupled with `id_value`, uniquely identifies the entity being scored. + id_value: + type: string + example: 'example.host' + description: The identifier value defining this risk score. Coupled with `id_field`, uniquely identifies the entity being scored. + calculated_level: + type: string + example: 'Critical' + description: Lexical description of the entity's risk. + calculated_score: + type: number + format: double + description: The raw numeric value of the given entity's risk score. + calculated_score_norm: + type: number + format: double + minimum: 0 + maximum: 100 + description: The normalized numeric value of the given entity's risk score. Useful for comparing with other entities. + category_1_score: + type: number + format: double + description: The contribution of Category 1 to the overall risk score (`calculated_score`). Category 1 contains Detection Engine Alerts. + category_1_count: + type: number + format: integer + description: The number of risk input documents that contributed to the Category 1 score (`category_1_score`). + inputs: + type: array + description: A list of the highest-risk documents contributing to this risk score. Useful for investigative purposes. + items: + $ref: '#/components/schemas/RiskScoreInput' + + + + RiskScoreWeight: + description: "Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1')." + type: object + required: + - type + properties: + type: + type: string + value: + type: string + host: + type: number + format: double + minimum: 0 + maximum: 1 + user: + type: number + format: double + minimum: 0 + maximum: 1 + example: + type: 'risk_category' + value: 'category_1' + host: 0.8 + user: 0.4 + + RiskScoreWeights: + description: 'A list of weights to be applied to the scoring calculation.' + type: array + items: + $ref: '#/components/schemas/RiskScoreWeight' + example: + - type: 'risk_category' + value: 'category_1' + host: 0.8 + user: 0.4 + - type: 'global_identifier' + host: 0.5 + user: 0.1 + + RiskEngineInitStep: + type: object + required: + - type + - success + properties: + type: + type: string + success: + type: boolean + error: + type: string diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts new file mode 100644 index 0000000000000..54c0e7f91b0ba --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.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 { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + */ + +import { DateRange } from '../common/common.gen'; + +export type RiskEngineSettingsResponse = z.infer; +export const RiskEngineSettingsResponse = z.object({ + range: DateRange.optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml new file mode 100644 index 0000000000000..aad2d49032856 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Risk Scoring API + description: These APIs allow the consumer to manage Entity Risk Scores within Entity Analytics. +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' + +paths: + /engine/settings: + get: + operationId: RiskEngineSettingsGet + summary: Get the settings of the Risk Engine + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/RiskEngineSettingsResponse' + +components: + schemas: + RiskEngineSettingsResponse: + type: object + properties: + range: + $ref: '../common/common.schema.yaml#/components/schemas/DateRange' \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts new file mode 100644 index 0000000000000..ba36ffbb71dfa --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './engine_settings_route.gen'; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e3fe732373798..4acadbd8e39ea 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -263,6 +263,7 @@ export const RISK_ENGINE_INIT_URL = `${RISK_ENGINE_URL}/init`; export const RISK_ENGINE_ENABLE_URL = `${RISK_ENGINE_URL}/enable`; export const RISK_ENGINE_DISABLE_URL = `${RISK_ENGINE_URL}/disable`; export const RISK_ENGINE_PRIVILEGES_URL = `${RISK_ENGINE_URL}/privileges`; +export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings`; export const ASSET_CRITICALITY_URL = `/internal/asset_criticality`; export const ASSET_CRITICALITY_PRIVILEGES_URL = `/internal/asset_criticality/privileges`; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 988bcb43ebf86..32ed3999bc747 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -17,6 +17,7 @@ import { ASSET_CRITICALITY_PRIVILEGES_URL, ASSET_CRITICALITY_URL, RISK_SCORE_INDEX_STATUS_API_URL, + RISK_ENGINE_SETTINGS_URL, } from '../../../common/constants'; import type { @@ -28,8 +29,8 @@ import type { } from '../../../server/lib/entity_analytics/types'; import type { RiskScorePreviewRequestSchema } from '../../../common/entity_analytics/risk_engine/risk_score_preview/request_schema'; import type { EntityAnalyticsPrivileges } from '../../../common/api/entity_analytics/common'; +import type { RiskEngineSettingsResponse } from '../../../common/api/entity_analytics/risk_engine'; import type { SnakeToCamelCase } from '../common/utils'; - import { useKibana } from '../../common/lib/kibana/kibana_react'; export const useEntityAnalyticsRoutes = () => { @@ -157,6 +158,15 @@ export const useEntityAnalyticsRoutes = () => { signal, }); + /** + * Fetches risk engine settings + */ + const fetchRiskEngineSettings = () => + http.fetch(RISK_ENGINE_SETTINGS_URL, { + version: '1', + method: 'GET', + }); + return { fetchRiskScorePreview, fetchRiskEngineStatus, @@ -168,6 +178,7 @@ export const useEntityAnalyticsRoutes = () => { createAssetCriticality, fetchAssetCriticality, getRiskScoreIndexStatus, + fetchRiskEngineSettings, }; }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_settings.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_settings.ts new file mode 100644 index 0000000000000..c67136ff8528a --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_settings.ts @@ -0,0 +1,13 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { useEntityAnalyticsRoutes } from '../api'; + +export const useRiskEngineSettings = () => { + const { fetchRiskEngineSettings } = useEntityAnalyticsRoutes(); + return useQuery(['GET', 'FETCH_RISK_ENGINE_SETTINGS'], fetchRiskEngineSettings); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.test.tsx index 8f61796f0861b..cee8cc0a4a0ec 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.test.tsx @@ -144,6 +144,13 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( ...defaultRisk, isModuleEnabled: false, refetch: result.current.refetch, + error: { + attributes: { + caused_by: { + type: 'index_not_found_exception', + }, + }, + }, }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.tsx b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.tsx index 3038e33d4bfab..d2d1ba522a877 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score.tsx @@ -41,6 +41,7 @@ export interface RiskScoreState { + it('should return query from host risk score', () => { + expect( + getAlertsQueryForRiskScore({ + riskScore: { + host: { + name: 'host-1', + risk, + }, + '@timestamp': '2023-08-10T14:00:00.000Z', + }, + riskRangeStart: 'now-30d', + }) + ).toEqual({ + _source: false, + size: 1000, + fields: ['*'], + query: { + bool: { + filter: [ + { term: { 'host.name': 'host-1' } }, + { + range: { + '@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' }, + }, + }, + ], + }, + }, + }); + }); + + it('should return query from user risk score', () => { + expect( + getAlertsQueryForRiskScore({ + riskScore: { + user: { + name: 'user-1', + risk, + }, + '@timestamp': '2023-08-10T14:00:00.000Z', + }, + riskRangeStart: 'now-30d', + }) + ).toEqual({ + _source: false, + size: 1000, + fields: ['*'], + query: { + bool: { + filter: [ + { term: { 'user.name': 'user-1' } }, + { + range: { + '@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' }, + }, + }, + ], + }, + }, + }); + }); + + it('should return query with custom fields', () => { + const query = getAlertsQueryForRiskScore({ + riskScore: { + user: { + name: 'user-1', + risk, + }, + '@timestamp': '2023-08-10T14:00:00.000Z', + }, + riskRangeStart: 'now-30d', + fields: ['event.category', 'event.action'], + }); + expect(query.fields).toEqual(['event.category', 'event.action']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts b/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts new file mode 100644 index 0000000000000..8a75aa19a227d --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + isUserRiskScore, + RiskScoreFields, +} from '../../../common/search_strategy/security_solution/risk_score/all'; +import type { + UserRiskScore, + HostRiskScore, +} from '../../../common/search_strategy/security_solution/risk_score/all'; +import { getStartDateFromRiskScore } from './get_start_date_from_risk_score'; + +const ALERTS_SIZE = 1000; + +/** + * return query to fetch alerts related to the risk score + */ +export const getAlertsQueryForRiskScore = ({ + riskRangeStart, + riskScore, + fields, +}: { + riskRangeStart: string; + riskScore: UserRiskScore | HostRiskScore; + fields?: string[]; +}) => { + let entityField: string; + let entityValue: string; + + if (isUserRiskScore(riskScore)) { + entityField = RiskScoreFields.userName; + entityValue = riskScore.user.name; + } else { + entityField = RiskScoreFields.hostName; + entityValue = riskScore.host.name; + } + + const from = getStartDateFromRiskScore({ + riskScoreTimestamp: riskScore['@timestamp'], + riskRangeStart, + }); + + const riskScoreTimestamp = riskScore['@timestamp']; + + return { + fields: fields || ['*'], + size: ALERTS_SIZE, + _source: false, + query: { + bool: { + filter: [ + { term: { [entityField]: entityValue } }, + { + range: { + '@timestamp': { + gte: from, + lte: riskScoreTimestamp, + }, + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.test.ts new file mode 100644 index 0000000000000..e94958985672f --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { getStartDateFromRiskScore } from './get_start_date_from_risk_score'; + +describe('getStartDateFromRiskScore', () => { + it('should return now-30d if there is an error in parsing the date', () => { + expect( + getStartDateFromRiskScore({ + riskRangeStart: 'aaa', + riskScoreTimestamp: '2023-08-10T14:00:00.000Z', + }) + ).toEqual('now-30d'); + }); + + it('should return start date from risk score timestamp and risk range start with days', () => { + expect( + getStartDateFromRiskScore({ + riskRangeStart: 'now-30d', + riskScoreTimestamp: '2023-08-10T14:00:00.000Z', + }) + ).toEqual('2023-07-11T14:00:00.000Z'); + }); + + it('should return start date from risk score timestamp and risk range start with hours', () => { + expect( + getStartDateFromRiskScore({ + riskRangeStart: 'now-8h', + riskScoreTimestamp: '2023-08-10T14:00:00.000Z', + }) + ).toEqual('2023-08-10T06:00:00.000Z'); + }); + + it('should return start date from risk score timestamp and risk range start with minutes', () => { + expect( + getStartDateFromRiskScore({ + riskRangeStart: 'now-10080m', + riskScoreTimestamp: '2023-08-10T14:00:00.000Z', + }) + ).toEqual('2023-08-03T14:00:00.000Z'); + }); + + it("should return risk range start if it's a date", () => { + expect( + getStartDateFromRiskScore({ + riskRangeStart: '2023-08-03T14:00:00.000Z', + riskScoreTimestamp: '2023-08-10T14:00:00.000Z', + }) + ).toEqual('2023-08-03T14:00:00.000Z'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.ts b/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.ts new file mode 100644 index 0000000000000..a3c7cfee663bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.ts @@ -0,0 +1,44 @@ +/* + * 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 dateMath from '@kbn/datemath'; +import moment from 'moment'; + +/** + * return start date of risk scoring by calculating the difference between risk score timestamp and risk range start date + * return the same risk range start date if it's a date + * return now-30d if there are erros in parsing the date + */ +export const getStartDateFromRiskScore = ({ + riskRangeStart, + riskScoreTimestamp, +}: { + riskRangeStart: string; + riskScoreTimestamp: string; +}): string => { + try { + if (moment(riskRangeStart).isValid()) { + return riskRangeStart; + } + const startDateFromNow = dateMath.parse(riskRangeStart); + if (!startDateFromNow || !startDateFromNow.isValid()) { + throw new Error('error parsing risk range start date'); + } + const now = moment(); + const rangeInHours = now.diff(startDateFromNow, 'minutes'); + + const riskScoreDate = dateMath.parse(riskScoreTimestamp); + if (!riskScoreDate || !riskScoreDate.isValid()) { + throw new Error('error parsing risk score timestamp'); + } + + const startDate = riskScoreDate.subtract(rangeInHours, 'minutes'); + + return startDate.utc().toISOString(); + } catch (error) { + return 'now-30d'; + } +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx index 64c79bfdb77f7..f984eb223ee11 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx @@ -9,11 +9,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { PREFIX } from '../../../flyout/shared/test_ids'; +import type { RiskInputsTabProps } from './tabs/risk_inputs'; import { RiskInputsTab } from './tabs/risk_inputs'; export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const; -export const getRiskInputTab = (alertIds: string[]) => ({ +export const getRiskInputTab = ({ entityType, entityName }: RiskInputsTabProps) => ({ id: EntityDetailsLeftPanelTab.RISK_INPUTS, 'data-test-subj': RISK_INPUTS_TAB_TEST_ID, name: ( @@ -22,5 +23,5 @@ export const getRiskInputTab = (alertIds: string[]) => ({ defaultMessage="Risk Inputs" /> ), - content: , + content: , }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.test.tsx index 038d6f7d622d4..d67ea7e30e438 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.test.tsx @@ -11,26 +11,50 @@ import { TestProviders } from '../../../../common/mock'; import { times } from 'lodash/fp'; import { RiskInputsTab } from './risk_inputs'; import { alertDataMock } from '../mocks'; +import { RiskSeverity } from '../../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../../common/entity_analytics/risk_engine'; -const mockUseAlertsByIds = jest.fn().mockReturnValue({ loading: false, data: [] }); +const mockUseRiskContributingAlerts = jest.fn().mockReturnValue({ loading: false, data: [] }); -jest.mock('../../../../common/containers/alerts/use_alerts_by_ids', () => ({ - useAlertsByIds: () => mockUseAlertsByIds(), +jest.mock('../../../hooks/use_risk_contributing_alerts', () => ({ + useRiskContributingAlerts: () => mockUseRiskContributingAlerts(), })); -const ALERT_IDS = ['123']; +const mockUseRiskScore = jest.fn().mockReturnValue({ loading: false, data: [] }); + +jest.mock('../../../api/hooks/use_risk_score', () => ({ + useRiskScore: () => mockUseRiskScore(), +})); + +const riskScore = { + '@timestamp': '2021-08-19T16:00:00.000Z', + user: { + name: 'elastic', + risk: { + rule_risks: [], + calculated_score_norm: 100, + multipliers: [], + calculated_level: RiskSeverity.critical, + }, + }, +}; describe('RiskInputsTab', () => { it('renders', () => { - mockUseAlertsByIds.mockReturnValue({ + mockUseRiskContributingAlerts.mockReturnValue({ loading: false, error: false, data: [alertDataMock], }); + mockUseRiskScore.mockReturnValue({ + loading: false, + error: false, + data: [riskScore], + }); const { getByTestId } = render( - + ); @@ -41,7 +65,6 @@ describe('RiskInputsTab', () => { }); it('paginates', () => { - const alertsIds = times((number) => number.toString(), 11); const alerts = times( (number) => ({ ...alertDataMock, @@ -50,15 +73,20 @@ describe('RiskInputsTab', () => { 11 ); - mockUseAlertsByIds.mockReturnValue({ + mockUseRiskContributingAlerts.mockReturnValue({ loading: false, error: false, data: alerts, }); + mockUseRiskScore.mockReturnValue({ + loading: false, + error: false, + data: [riskScore], + }); const { getAllByTestId, getByLabelText } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.tsx index b45f47306c08d..69cf023b3a44b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs.tsx @@ -6,18 +6,22 @@ */ import type { EuiBasicTableColumn, Pagination } from '@elastic/eui'; -import { EuiSpacer, EuiInMemoryTable, EuiTitle } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable, EuiTitle, EuiCallOut } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { get } from 'lodash/fp'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; -import { useAlertsByIds } from '../../../../common/containers/alerts/use_alerts_by_ids'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { ActionColumn } from '../components/action_column'; import { RiskInputsUtilityBar } from '../components/utility_bar'; +import { useRiskContributingAlerts } from '../../../hooks/use_risk_contributing_alerts'; +import { useRiskScore } from '../../../api/hooks/use_risk_score'; +import { buildHostNamesFilter, buildUserNamesFilter } from '../../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../../common/entity_analytics/risk_engine'; export interface RiskInputsTabProps extends Record { - alertIds: string[]; + entityType: RiskScoreEntity; + entityName: string; } export interface AlertRawData { @@ -26,9 +30,36 @@ export interface AlertRawData { _id: string; } -export const RiskInputsTab = ({ alertIds }: RiskInputsTabProps) => { +const FIRST_RECORD_PAGINATION = { + cursorStart: 0, + querySize: 1, +}; + +export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => { const [selectedItems, setSelectedItems] = useState([]); - const { loading, data: alertsData } = useAlertsByIds({ alertIds }); + + const nameFilterQuery = useMemo(() => { + if (entityType === RiskScoreEntity.host) { + return buildHostNamesFilter([entityName]); + } else if (entityType === RiskScoreEntity.user) { + return buildUserNamesFilter([entityName]); + } + }, [entityName, entityType]); + + const { data: riskScoreData, error: riskScoreError } = useRiskScore({ + riskEntity: entityType, + filterQuery: nameFilterQuery, + onlyLatest: false, + pagination: FIRST_RECORD_PAGINATION, + skip: nameFilterQuery === undefined, + }); + + const riskScore = riskScoreData && riskScoreData.length > 0 ? riskScoreData[0] : undefined; + const { + loading, + data: alertsData, + error: riskAlertsError, + } = useRiskContributingAlerts({ riskScore }); const euiTableSelectionProps = useMemo( () => ({ @@ -98,13 +129,35 @@ export const RiskInputsTab = ({ alertIds }: RiskInputsTabProps) => { const pagination: Pagination = useMemo( () => ({ - totalItemCount: alertIds.length, + totalItemCount: alertsData?.length ?? 0, pageIndex: currentPage.index, pageSize: currentPage.size, }), - [currentPage.index, currentPage.size, alertIds.length] + [currentPage.index, currentPage.size, alertsData?.length] ); + if (riskScoreError || riskAlertsError) { + return ( + + } + color="danger" + iconType="error" + > +

+ +

+
+ ); + } + return ( <> {/* Temporary label. It will be replaced by a filter */} diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index b0ee36eff0747..bd833bc68b016 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -18,6 +18,17 @@ import type { VisualizationEmbeddableProps, } from '../../../common/components/visualization_actions/types'; +const mockContributingAlerts = Array(6).fill({}); +const expectedRiskInputsLength = mockContributingAlerts.length; + +const mockUseRiskContributingAlerts = jest + .fn() + .mockReturnValue({ loading: false, data: mockContributingAlerts }); + +jest.mock('../../hooks/use_risk_contributing_alerts', () => ({ + useRiskContributingAlerts: () => mockUseRiskContributingAlerts(), +})); + const mockVisualizationEmbeddable = jest .fn() .mockReturnValue(
); @@ -44,7 +55,9 @@ describe('RiskSummary', () => { ); expect(getByTestId('risk-summary-table')).toBeInTheDocument(); - expect(getByTestId('risk-summary-table')).toHaveTextContent('Inputs1'); + expect(getByTestId('risk-summary-table')).toHaveTextContent( + `Inputs${expectedRiskInputsLength}` + ); expect(getByTestId('risk-summary-table')).toHaveTextContent('CategoryAlerts'); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index c8d4f093fdc4d..8a15ec3cd435c 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -34,6 +34,7 @@ import { VisualizationEmbeddable } from '../../../common/components/visualizatio import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; import type { RiskScoreState } from '../../api/hooks/use_risk_score'; import { getRiskScoreSummaryAttributes } from '../../lens_attributes/risk_score_summary'; +import { useRiskContributingAlerts } from '../../hooks/use_risk_contributing_alerts'; export interface RiskSummaryProps { riskScoreData: RiskScoreState; @@ -47,6 +48,7 @@ interface TableItem { } const LENS_VISUALIZATION_HEIGHT = 126; // Static height in pixels specified by design const LAST_30_DAYS = { from: 'now-30d', to: 'now' }; +const ALERTS_FIELDS: string[] = []; function isUserRiskData( riskData: UserRiskScore | HostRiskScore | undefined @@ -75,7 +77,10 @@ const RiskSummaryComponent = ({ const riskData = data && data.length > 0 ? data[0] : undefined; const entityData = getEntityData(riskData); const { euiTheme } = useEuiTheme(); - + const { data: alertsData } = useRiskContributingAlerts({ + riskScore: riskData, + fields: ALERTS_FIELDS, + }); const lensAttributes = useMemo(() => { const entityName = entityData?.name ?? ''; const fieldName = isUserRiskData(riskData) ? 'user.name' : 'host.name'; @@ -127,10 +132,10 @@ const RiskSummaryComponent = ({ category: i18n.translate('xpack.securitySolution.flyout.entityDetails.alertsGroupLabel', { defaultMessage: 'Alerts', }), - count: entityData?.risk.inputs?.length ?? 0, + count: alertsData?.length ?? 0, }, ], - [entityData?.risk.inputs?.length] + [alertsData?.length] ); return ( diff --git a/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts b/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts new file mode 100644 index 0000000000000..f315908be2a71 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.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 { useEffect } from 'react'; +import { useQueryAlerts } from '../../detections/containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../detections/containers/detection_engine/alerts/constants'; + +import type { + UserRiskScore, + HostRiskScore, +} from '../../../common/search_strategy/security_solution/risk_score/all'; +import { getAlertsQueryForRiskScore } from '../common/get_alerts_query_for_risk_score'; + +import { useRiskEngineSettings } from '../api/hooks/use_risk_engine_settings'; + +interface UseRiskContributingAlerts { + riskScore: UserRiskScore | HostRiskScore | undefined; + fields?: string[]; +} + +interface Hit { + fields: Record; + _index: string; + _id: string; +} + +interface UseRiskContributingAlertsResult { + loading: boolean; + error: boolean; + data?: Hit[]; +} + +/** + * Fetches alerts related to the risk score + */ +export const useRiskContributingAlerts = ({ + riskScore, + fields, +}: UseRiskContributingAlerts): UseRiskContributingAlertsResult => { + const { data: riskEngineSettings } = useRiskEngineSettings(); + + const { loading, data, setQuery } = useQueryAlerts({ + // is empty query, to skip fetching alert, until we have risk engine settings + query: {}, + queryName: ALERTS_QUERY_NAMES.BY_ID, + }); + + useEffect(() => { + if (!riskEngineSettings?.range?.start || !riskScore) return; + + setQuery( + getAlertsQueryForRiskScore({ + riskRangeStart: riskEngineSettings.range.start, + riskScore, + fields, + }) + ); + }, [setQuery, riskScore, riskEngineSettings?.range?.start, fields]); + + const error = !loading && data === undefined; + + return { + loading, + error, + data: data?.hits.hits, + }; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx index 82cf0f8ff9f56..6d2c7121b6ad3 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx @@ -10,29 +10,38 @@ import { render } from '@testing-library/react'; import React from 'react'; import { HostDetailsPanel } from '.'; import { TestProviders } from '../../../common/mock'; +import { RiskSeverity } from '../../../../common/search_strategy'; + +const riskScore = { + '@timestamp': '2021-08-19T16:00:00.000Z', + host: { + name: 'elastic', + risk: { + rule_risks: [], + calculated_score_norm: 100, + multipliers: [], + calculated_level: RiskSeverity.critical, + }, + }, +}; +const mockUseRiskScore = jest.fn().mockReturnValue({ loading: false, data: [riskScore] }); + +jest.mock('../../../entity_analytics/api/hooks/use_risk_score', () => ({ + useRiskScore: () => mockUseRiskScore(), +})); describe('HostDetailsPanel', () => { it('render risk inputs panel', () => { - const { getByTestId } = render( - , - { wrapper: TestProviders } - ); + const { getByTestId } = render(, { + wrapper: TestProviders, + }); expect(getByTestId(RISK_INPUTS_TAB_TEST_ID)).toBeInTheDocument(); }); it("doesn't render risk inputs panel when no alerts ids are provided", () => { - const { queryByTestId } = render( - , - { wrapper: TestProviders } - ); + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); expect(queryByTestId(RISK_INPUTS_TAB_TEST_ID)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index 3214dec23bdd6..853a68c4fb95e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -13,13 +13,11 @@ import { EntityDetailsLeftPanelTab, LeftPanelHeader, } from '../shared/components/left_panel/left_panel_header'; - -interface RiskInputsParam { - alertIds: string[]; -} +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; export interface HostDetailsPanelProps extends Record { - riskInputs: RiskInputsParam; + isRiskScoreExist: boolean; + name: string; } export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'host_details'; @@ -27,15 +25,18 @@ export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { } export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details'; -export const HostDetailsPanel = ({ riskInputs }: HostDetailsPanelProps) => { +export const HostDetailsPanel = ({ name, isRiskScoreExist }: HostDetailsPanelProps) => { // Temporary implementation while Host details left panel don't have Asset tabs const [tabs, selectedTabId, setSelectedTabId] = useMemo(() => { + const isRiskScoreTabAvailable = isRiskScoreExist && name; return [ - riskInputs.alertIds.length > 0 ? [getRiskInputTab(riskInputs.alertIds)] : [], + isRiskScoreTabAvailable + ? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host })] + : [], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}, ]; - }, [riskInputs.alertIds]); + }, [name, isRiskScoreExist]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 783a9ce598381..51127f25fb2cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -64,6 +64,7 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan const { data: hostRisk, inspect: inspectRiskScore, refetch, loading } = riskScoreState; const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; + const isRiskScoreExist = !!hostRiskData?.host.risk; useQueryInspector({ deleteQuery, @@ -79,17 +80,13 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan openLeftPanel({ id: HostDetailsPanelKey, params: { - riskInputs: { - alertIds: hostRiskData?.host.risk.inputs?.map(({ id }) => id) ?? [], - host: { - name: hostName, - }, - }, + name: hostName, + isRiskScoreExist, path: tab ? { tab } : undefined, }, }); }, - [openLeftPanel, hostRiskData?.host.risk.inputs, hostName] + [openLeftPanel, hostName, isRiskScoreExist] ); const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]); @@ -119,7 +116,7 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts index 01dafb9d6b47a..9c7671917bbd9 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts @@ -81,6 +81,7 @@ export const mockUserRiskScoreState: RiskScoreState = { isAuthorized: true, isDeprecated: false, loading: false, + error: undefined, }; export const mockHostRiskScoreState: RiskScoreState = { @@ -96,6 +97,7 @@ export const mockHostRiskScoreState: RiskScoreState = { isAuthorized: true, isDeprecated: false, loading: false, + error: undefined, }; const hostMetadata: HostMetadataInterface = { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index c2591eab2c914..4c0221dba02bc 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -18,17 +18,13 @@ import type { import { LeftPanelHeader } from '../shared/components/left_panel/left_panel_header'; import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content'; -interface RiskInputsParam { - alertIds: string[]; -} - interface UserParam { name: string; email: string[]; } export interface UserDetailsPanelProps extends Record { - riskInputs: RiskInputsParam; + isRiskScoreExist: boolean; user: UserParam; path?: PanelPath; } @@ -38,10 +34,10 @@ export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps { } export const UserDetailsPanelKey: UserDetailsExpandableFlyoutProps['key'] = 'user_details'; -export const UserDetailsPanel = ({ riskInputs, user, path }: UserDetailsPanelProps) => { +export const UserDetailsPanel = ({ isRiskScoreExist, user, path }: UserDetailsPanelProps) => { const managedUser = useManagedUser(user.name, user.email); - const tabs = useTabs(managedUser.data, riskInputs.alertIds); - const { selectedTabId, setSelectedTabId } = useSelectedTab(riskInputs, user, tabs, path); + const tabs = useTabs(managedUser.data, user.name, isRiskScoreExist); + const { selectedTabId, setSelectedTabId } = useSelectedTab(isRiskScoreExist, user, tabs, path); if (managedUser.isLoading) return ; @@ -58,7 +54,7 @@ export const UserDetailsPanel = ({ riskInputs, user, path }: UserDetailsPanelPro }; const useSelectedTab = ( - riskInputs: RiskInputsParam, + isRiskScoreExist: boolean, user: UserParam, tabs: LeftPanelTabsType, path: PanelPath | undefined @@ -79,8 +75,8 @@ const useSelectedTab = ( tab: tabId, }, params: { - riskInputs, user, + isRiskScoreExist, }, }); }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 3867afb4470e2..e00d6bdd365c0 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -18,17 +18,27 @@ import type { import { ENTRA_TAB_TEST_ID, OKTA_TAB_TEST_ID } from './test_ids'; import { AssetDocumentTab } from './tabs/asset_document'; import { RightPanelProvider } from '../../document_details/right/context'; +import { RiskScoreEntity } from '../../../../common/search_strategy'; import type { LeftPanelTabsType } from '../shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; -export const useTabs = (managedUser: ManagedUserHits, alertIds: string[]): LeftPanelTabsType => +export const useTabs = ( + managedUser: ManagedUserHits, + name: string, + isRiskScoreExist: boolean +): LeftPanelTabsType => useMemo(() => { const tabs: LeftPanelTabsType = []; const entraManagedUser = managedUser[ManagedUserDatasetKey.ENTRA]; const oktaManagedUser = managedUser[ManagedUserDatasetKey.OKTA]; - if (alertIds.length > 0) { - tabs.push(getRiskInputTab(alertIds)); + if (isRiskScoreExist) { + tabs.push( + getRiskInputTab({ + entityName: name, + entityType: RiskScoreEntity.user, + }) + ); } if (oktaManagedUser) { @@ -40,7 +50,7 @@ export const useTabs = (managedUser: ManagedUserHits, alertIds: string[]): LeftP } return tabs; - }, [alertIds, managedUser]); + }, [isRiskScoreExist, managedUser, name]); const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({ id: EntityDetailsLeftPanelTab.OKTA, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index abe3ee4793016..843cd3111cfb5 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -83,9 +83,7 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan openLeftPanel({ id: UserDetailsPanelKey, params: { - riskInputs: { - alertIds: userRiskData?.user.risk.inputs?.map(({ id }) => id) ?? [], - }, + isRiskScoreExist: !!userRiskData?.user?.risk, user: { name: userName, email, @@ -94,7 +92,7 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan path: tab ? { tab } : undefined, }); }, - [email, openLeftPanel, userName, userRiskData?.user.risk.inputs] + [email, openLeftPanel, userName, userRiskData] ); const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts index 43abce1104467..da5f0f4c6440e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts @@ -41,6 +41,7 @@ export const mockRiskScoreState = { isAuthorized: true, isDeprecated: false, loading: false, + error: undefined, }; export const mockOktaUserFields: ManagedUserFields = { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts index 7e16ed57946e5..df74b846ed464 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts @@ -10,3 +10,4 @@ export { riskEngineEnableRoute } from './enable'; export { riskEngineDisableRoute } from './disable'; export { riskEngineStatusRoute } from './status'; export { riskEnginePrivilegesRoute } from './privileges'; +export { riskEngineSettingsRoute } from './settings'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts new file mode 100644 index 0000000000000..50228a5542bd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts @@ -0,0 +1,49 @@ +/* + * 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 { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { RISK_ENGINE_SETTINGS_URL, APP_ID } from '../../../../../common/constants'; + +import type { SecuritySolutionPluginRouter } from '../../../../types'; + +export const riskEngineSettingsRoute = (router: SecuritySolutionPluginRouter) => { + router.versioned + .get({ + access: 'internal', + path: RISK_ENGINE_SETTINGS_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion({ version: '1', validate: {} }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const securitySolution = await context.securitySolution; + const riskEngineClient = securitySolution.getRiskEngineDataClient(); + + try { + const result = await riskEngineClient.getConfiguration(); + if (!result) { + throw new Error('Unable to get risk engine configuration'); + } + return response.ok({ + body: { + range: result.range, + }, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: { message: error.message, full_error: JSON.stringify(e) }, + bypassErrorFormat: true, + }); + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml index d9840840ea2f6..02471038ddb38 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml @@ -103,6 +103,16 @@ paths: application/json: schema: $ref: '#/components/schemas/RiskEnginePrivilegesResponse' + /engine/settings: + get: + summary: Get the settings of the Risk Engine + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/RiskEngineSettingsResponse' components: @@ -468,3 +478,8 @@ components: has_all_required: description: If true then the user has full access to the risk engine type: boolean + RiskEngineSettingsResponse: + type: object + properties: + range: + $ref: '#/components/schemas/DateRange' diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index b32543ad7612d..6610a7e3f31a0 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -64,6 +64,7 @@ import { riskEngineEnableRoute, riskEngineStatusRoute, riskEnginePrivilegesRoute, + riskEngineSettingsRoute, } from '../lib/entity_analytics/risk_engine/routes'; import { registerTimelineRoutes } from '../lib/timeline/routes'; import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_score/routes/calculation'; @@ -165,6 +166,7 @@ export const initRoutes = ( riskEngineInitRoute(router, getStartServices); riskEngineEnableRoute(router, getStartServices); riskEngineDisableRoute(router, getStartServices); + riskEngineSettingsRoute(router); if (config.experimentalFeatures.riskEnginePrivilegesRouteEnabled) { riskEnginePrivilegesRoute(router, getStartServices); }