From 3fea5d4bbeb8871f356bb2344ea70c79cfaef112 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 9 Jan 2024 13:21:48 +0100 Subject: [PATCH] Update the Risk Input table in the expanded Entity Flyout to incorporate all alerts contributing to the risk score (#174025) ## Update the Risk Input table in the expanded Entity Flyout to incorporate all alerts contributing to the risk score Screenshot 2024-01-03 at 12 06 13 We have changed the way how we request alerts for the risk inputs table. It will create a new query: ``` query: { bool: { filter: [ { term: { "user.name": "${user.name}" } }, { range: { '@timestamp': { gte: from, lte: to, }, }, }, ], }, } ``` It still uses in-memory pagination. It also updates the number of alerts in the Risk input summary but doesn't change the inspect query, as it can be used in the future for asset criticality and other input sources. ## Add new API to return risk engine settings Currently, return only the risk engine **range**, any other options can be added later if needed. Also added an open API generated types for this route --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../api/entity_analytics/common/common.gen.ts | 136 +++++++++++++ .../common/common.schema.yaml | 192 ++++++++++++++++++ .../risk_engine/engine_settings_route.gen.ts | 20 ++ .../engine_settings_route.schema.yaml | 33 +++ .../api/entity_analytics/risk_engine/index.ts | 8 + .../security_solution/common/constants.ts | 1 + .../public/entity_analytics/api/api.ts | 13 +- .../api/hooks/use_risk_engine_settings.ts | 13 ++ .../api/hooks/use_risk_score.test.tsx | 7 + .../api/hooks/use_risk_score.tsx | 13 +- .../get_alerts_query_for_risk_score.test.ts | 94 +++++++++ .../common/get_alerts_query_for_risk_score.ts | 69 +++++++ .../get_start_date_from_risk_score.test.ts | 54 +++++ .../common/get_start_date_from_risk_score.ts | 44 ++++ .../entity_details_flyout/index.tsx | 5 +- .../tabs/risk_inputs.test.tsx | 46 ++++- .../tabs/risk_inputs.tsx | 67 +++++- .../risk_summary_flyout/risk_summary.test.tsx | 15 +- .../risk_summary_flyout/risk_summary.tsx | 11 +- .../hooks/use_risk_contributing_alerts.ts | 71 +++++++ .../host_details_left/index.test.tsx | 41 ++-- .../host_details_left/index.tsx | 17 +- .../entity_details/host_right/index.tsx | 13 +- .../flyout/entity_details/mocks/index.ts | 2 + .../user_details_left/index.tsx | 16 +- .../entity_details/user_details_left/tabs.tsx | 18 +- .../entity_details/user_right/index.tsx | 6 +- .../new_user_detail/__mocks__/index.ts | 1 + .../risk_engine/routes/index.ts | 1 + .../risk_engine/routes/settings.ts | 49 +++++ .../risk_engine/schema/risk_score_apis.yml | 15 ++ .../security_solution/server/routes/index.ts | 2 + 32 files changed, 1019 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_settings_route.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/index.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_settings.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.test.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.test.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_start_date_from_risk_score.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/settings.ts 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); }