From 348ea81b1e85e846c855635c51e4e7fc3136bda1 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 17 Dec 2024 10:39:08 -0500 Subject: [PATCH 01/13] =?UTF-8?q?[Fleet]=20Bump=20install=20version=20afte?= =?UTF-8?q?r=20removal=20of=20deprecated=20=5Fsource.mode=E2=80=A6=20(#204?= =?UTF-8?q?471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- x-pack/plugins/fleet/server/constants/fleet_es_assets.ts | 2 +- .../steps/step_save_system_object.test.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index 621adc5b3b81c..384a77afd112e 100644 --- a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -11,7 +11,7 @@ import { getESAssetMetadata } from '../services/epm/elasticsearch/meta'; const meta = getESAssetMetadata(); -export const FLEET_INSTALL_FORMAT_VERSION = '1.3.0'; +export const FLEET_INSTALL_FORMAT_VERSION = '1.4.0'; export const FLEET_AGENT_POLICIES_SCHEMA_VERSION = '1.1.1'; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.test.ts index 8d80c236aefb0..c4ae211c58fc5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_system_object.test.ts @@ -13,14 +13,12 @@ import { } from '@kbn/core/server/mocks'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import { FLEET_INSTALL_FORMAT_VERSION } from '../../../../../constants'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../../common/constants'; - import { appContextService } from '../../../../app_context'; import { createAppContextStartContractMock } from '../../../../../mocks'; - import { auditLoggingService } from '../../../../audit_logging'; import { packagePolicyService } from '../../../../package_policy'; - import { createArchiveIteratorFromMap } from '../../../archive/archive_iterator'; import { stepSaveSystemObject } from './step_save_system_object'; @@ -94,7 +92,7 @@ describe('updateLatestExecutedState', () => { 'epm-packages', 'test-integration', { - install_format_schema_version: '1.3.0', + install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION, install_status: 'installed', install_version: '1.0.0', latest_install_failed_attempts: [], @@ -161,7 +159,7 @@ describe('updateLatestExecutedState', () => { 'epm-packages', 'test-integration', { - install_format_schema_version: '1.3.0', + install_format_schema_version: FLEET_INSTALL_FORMAT_VERSION, install_status: 'installed', install_version: '1.0.0', latest_install_failed_attempts: [], From 59826ccdb55775261d2255d1483c40b3fbf60a21 Mon Sep 17 00:00:00 2001 From: Arturo Castillo Delgado Date: Tue, 17 Dec 2024 16:49:36 +0100 Subject: [PATCH 02/13] [Discover] Fix console warning for invalid DOM property (#204417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes one of the errors described in #202287 — namely a console warning due to invalid DOM property. --- .../public/components/doc_viewer_flyout/use_flyout_a11y.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/use_flyout_a11y.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/use_flyout_a11y.tsx index 84c47cc4bd063..18cc98fb94b89 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/use_flyout_a11y.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/use_flyout_a11y.tsx @@ -37,7 +37,7 @@ export const useFlyoutA11y = ({ isXlScreen }: { isXlScreen: boolean }) => { a11yProps: { ref: setFlyoutEl, role: isXlScreen ? 'dialog' : undefined, - tabindex: isXlScreen ? 0 : undefined, + tabIndex: isXlScreen ? 0 : undefined, 'aria-describedby': isXlScreen ? descriptionId : undefined, 'data-no-focus-lock': isXlScreen || undefined, }, From a7addbadd394b33fa3212e57f55be251c1a3a371 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Tue, 17 Dec 2024 16:53:03 +0100 Subject: [PATCH 03/13] [EDR Workflows] CrowdStrike RunScript: Log Actions and UI Output (#204044) --- .../common/crowdstrike/schema.ts | 28 +++-- .../common/crowdstrike/types.ts | 5 +- .../crowdstrike/crowdstrike.ts | 12 +- .../actions/common/response_actions.ts | 2 + .../response_actions/run_script/run_script.ts | 12 +- .../service/response_actions/type_guards.ts | 11 ++ .../common/endpoint/types/actions.ts | 3 +- .../public/management/common/translations.ts | 2 +- .../execute_action_host_response.tsx | 33 ++++-- .../execute_action_host_response_output.tsx | 23 ++-- .../run_script_action.tsx | 96 ++++++++++++++++ .../lib/console_commands_definition.ts | 5 +- .../components/action_log_expanded_tray.tsx | 25 +++++ ...e_send_run_script_endpoint_request.test.ts | 81 ++++++++++++++ .../use_send_run_script_endpoint_request.ts | 40 +++++++ .../mocks/response_actions_http_mocks.ts | 24 ++++ .../endpoint/common/response_actions.ts | 10 ++ .../crowdstrike/crowdstrike_actions_client.ts | 103 ++++++++++++++++-- .../actions/clients/crowdstrike/utils.test.ts | 51 +++++++++ .../actions/clients/crowdstrike/utils.ts | 43 ++++++++ .../services/actions/clients/mocks.ts | 2 +- 21 files changed, 546 insertions(+), 65 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts index d5d4154f985e9..40e58613c69e0 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts @@ -318,16 +318,16 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object( schema.string(), schema.object( { - session_id: schema.maybe(schema.string()), - task_id: schema.maybe(schema.string()), - complete: schema.maybe(schema.boolean()), - stdout: schema.maybe(schema.string()), - stderr: schema.maybe(schema.string()), - base_command: schema.maybe(schema.string()), - aid: schema.maybe(schema.string()), - errors: schema.maybe(schema.arrayOf(schema.any())), - query_time: schema.maybe(schema.number()), - offline_queued: schema.maybe(schema.boolean()), + session_id: schema.string(), + task_id: schema.string(), + complete: schema.boolean(), + stdout: schema.string(), + stderr: schema.string(), + base_command: schema.string(), + aid: schema.string(), + errors: schema.arrayOf(schema.any()), + query_time: schema.number(), + offline_queued: schema.boolean(), }, { unknowns: 'allow' } ) @@ -337,9 +337,9 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object( ), meta: schema.object( { - query_time: schema.maybe(schema.number()), - powered_by: schema.maybe(schema.string()), - trace_id: schema.maybe(schema.string()), + query_time: schema.number(), + powered_by: schema.string(), + trace_id: schema.string(), }, { unknowns: 'allow' } ), @@ -348,7 +348,5 @@ export const CrowdstrikeExecuteRTRResponseSchema = schema.object( { unknowns: 'allow' } ); -export type CrowdStrikeExecuteRTRResponse = typeof CrowdstrikeExecuteRTRResponseSchema; - // TODO: will be part of a next PR export const CrowdstrikeGetScriptsParamsSchema = schema.any({}); diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts index 3c9cc15ea167e..68f8275d71434 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts @@ -17,8 +17,8 @@ import { CrowdstrikeGetTokenResponseSchema, CrowdstrikeGetAgentsResponseSchema, RelaxedCrowdstrikeBaseApiResponseSchema, - CrowdstrikeInitRTRResponseSchema, CrowdstrikeInitRTRParamsSchema, + CrowdstrikeExecuteRTRResponseSchema, } from './schema'; export type CrowdstrikeConfig = TypeOf; @@ -35,9 +35,10 @@ export type CrowdstrikeGetAgentOnlineStatusResponse = TypeOf< typeof CrowdstrikeGetAgentOnlineStatusResponseSchema >; export type CrowdstrikeGetTokenResponse = TypeOf; -export type CrowdstrikeInitRTRResponse = TypeOf; export type CrowdstrikeHostActionsParams = TypeOf; export type CrowdstrikeActionParams = TypeOf; export type CrowdstrikeInitRTRParams = TypeOf; + +export type CrowdStrikeExecuteRTRResponse = TypeOf; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts index 14b38b414eb3b..9bc53c58aa198 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts @@ -22,6 +22,7 @@ import type { CrowdstrikeGetTokenResponse, CrowdstrikeGetAgentOnlineStatusResponse, RelaxedCrowdstrikeBaseApiResponse, + CrowdStrikeExecuteRTRResponse, } from '../../../common/crowdstrike/types'; import { CrowdstrikeHostActionsParamsSchema, @@ -31,7 +32,6 @@ import { CrowdstrikeRTRCommandParamsSchema, CrowdstrikeExecuteRTRResponseSchema, CrowdstrikeGetScriptsParamsSchema, - CrowdStrikeExecuteRTRResponse, CrowdstrikeApiDoNotValidateResponsesSchema, CrowdstrikeGetTokenResponseSchema, } from '../../../common/crowdstrike/schema'; @@ -292,15 +292,9 @@ export class CrowdstrikeConnector extends SubActionConnector< payload: { command: string; endpoint_ids: string[]; - overwriteUrl?: 'batchExecuteRTR' | 'batchActiveResponderExecuteRTR' | 'batchAdminExecuteRTR'; }, connectorUsageCollector: ConnectorUsageCollector ): Promise => { - // Some commands are only available in specific API endpoints, however there's an additional requirement check for the command's argument - // Eg. runscript command is available with the batchExecuteRTR endpoint, but if it goes with --Raw parameter, it should go to batchAdminExecuteRTR endpoint - // This overwrite value will be coming from kibana response actions api - const csUrl = payload.overwriteUrl ? this.urls[payload.overwriteUrl] : url; - const batchId = await this.crowdStrikeSessionManager.initializeSession( { endpoint_ids: payload.endpoint_ids }, connectorUsageCollector @@ -313,7 +307,7 @@ export class CrowdstrikeConnector extends SubActionConnector< } return await this.crowdstrikeApiRequest( { - url: csUrl, + url, method: 'post', data: { base_command: baseCommand, @@ -335,7 +329,6 @@ export class CrowdstrikeConnector extends SubActionConnector< payload: { command: string; endpoint_ids: string[]; - overwriteUrl?: 'batchActiveResponderExecuteRTR' | 'batchAdminExecuteRTR'; }, connectorUsageCollector: ConnectorUsageCollector ): Promise { @@ -351,7 +344,6 @@ export class CrowdstrikeConnector extends SubActionConnector< payload: { command: string; endpoint_ids: string[]; - overwriteUrl?: 'batchAdminExecuteRTR'; }, connectorUsageCollector: ConnectorUsageCollector ): Promise { diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts index 56b1fafdb5a71..3a765e0c83134 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/common/response_actions.ts @@ -17,6 +17,7 @@ import { GetProcessesRouteRequestSchema } from '../response_actions/running_proc import { KillProcessRouteRequestSchema } from '../response_actions/kill_process'; import { SuspendProcessRouteRequestSchema } from '../response_actions/suspend_process'; import { UploadActionRequestSchema } from '../response_actions/upload'; +import { RunScriptActionRequestSchema } from '../response_actions/run_script'; export const ResponseActionBodySchema = schema.oneOf([ IsolateRouteRequestSchema.body, @@ -28,6 +29,7 @@ export const ResponseActionBodySchema = schema.oneOf([ ExecuteActionRequestSchema.body, UploadActionRequestSchema.body, ScanActionRequestSchema.body, + RunScriptActionRequestSchema.body, ]); export type ResponseActionsRequestBody = TypeOf; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts index dfa88941b34e0..95c035e866884 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/actions/response_actions/run_script/run_script.ts @@ -26,27 +26,27 @@ export const RunScriptActionRequestSchema = { /** * The script to run */ - Raw: schema.maybe(NonEmptyString), + raw: schema.maybe(NonEmptyString), /** * The path to the script on the host to run */ - HostPath: schema.maybe(NonEmptyString), + hostPath: schema.maybe(NonEmptyString), /** * The path to the script in the cloud to run */ - CloudFile: schema.maybe(NonEmptyString), + cloudFile: schema.maybe(NonEmptyString), /** * The command line to run */ - CommandLine: schema.maybe(NonEmptyString), + commandLine: schema.maybe(NonEmptyString), /** * The max timeout value before the command is killed. Number represents milliseconds */ - Timeout: schema.maybe(schema.number({ min: 1 })), + timeout: schema.maybe(schema.number({ min: 1 })), }, { validate: (params) => { - if (!params.Raw && !params.HostPath && !params.CloudFile) { + if (!params.raw && !params.hostPath && !params.cloudFile) { return 'At least one of Raw, HostPath, or CloudFile must be provided'; } }, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts index 707be0a4d1e65..ece0a9501e3fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts @@ -15,6 +15,8 @@ import type { ResponseActionUploadOutputContent, ResponseActionUploadParameters, GetProcessesActionOutputContent, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, } from '../../types'; import { RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_TYPE } from './constants'; @@ -47,6 +49,15 @@ export const isProcessesAction = ( return action.command === 'running-processes'; }; +export const isRunScriptAction = ( + action: MaybeImmutable +): action is ActionDetails< + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters +> => { + return action.command === 'runscript'; +}; + // type guards to ensure only the matching string values are attached to the types filter type export const isAgentType = (type: string): type is (typeof RESPONSE_ACTION_AGENT_TYPE)[number] => RESPONSE_ACTION_AGENT_TYPE.includes(type as (typeof RESPONSE_ACTION_AGENT_TYPE)[number]); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts index 131a8d0c6df5c..e3c47102ce719 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/actions.ts @@ -97,7 +97,8 @@ export interface ResponseActionScanOutputContent { } export interface ResponseActionRunScriptOutputContent { - output: string; + stdout: string; + stderr: string; code: string; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts index 6929452e5d9e9..50885a9db6b6a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts @@ -279,7 +279,7 @@ Command Examples for Running Scripts: 3. Executes a raw script provided entirely within the "--Raw" flag. - runscript --Raw="Get-ChildItem." + runscript --Raw=\`\`\`Get-ChildItem.\`\`\` 4. Executes a script located on the remote host at the specified path with the provided command-line arguments. diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx index 43ff3ffca5a62..09d1e45f24c45 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response.tsx @@ -10,6 +10,8 @@ import type { ActionDetails, MaybeImmutable, ResponseActionExecuteOutputContent, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, ResponseActionsExecuteParameters, } from '../../../../common/endpoint/types'; import { EXECUTE_FILE_LINK_TITLE } from '../endpoint_response_actions_list/translations'; @@ -18,14 +20,18 @@ import { ExecuteActionHostResponseOutput } from './execute_action_host_response_ export interface ExecuteActionHostResponseProps { action: MaybeImmutable< - ActionDetails + | ActionDetails + | ActionDetails >; agentId?: string; canAccessFileDownloadLink: boolean; 'data-test-subj'?: string; textSize?: 'xs' | 's'; + hideFile?: boolean; + hideContext?: boolean; } +// Note: also used for RunScript command export const ExecuteActionHostResponse = memo( ({ action, @@ -33,6 +39,8 @@ export const ExecuteActionHostResponse = memo( canAccessFileDownloadLink, textSize = 's', 'data-test-subj': dataTestSubj, + hideFile, + hideContext, }) => { const outputContent = useMemo( () => @@ -44,21 +52,24 @@ export const ExecuteActionHostResponse = memo( return ( <> - - - - + {!hideFile && ( + + + + + )} {outputContent && ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx index f24f18e149393..4e6161b8767d9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_execute_action/execute_action_host_response_output.tsx @@ -98,6 +98,7 @@ interface ShellInfoContentProps { textSize?: 's' | 'xs'; title: string; } + const ShellInfoContent = memo(({ content, textSize, title }) => ( @@ -178,10 +179,12 @@ export interface ExecuteActionHostResponseOutputProps { outputContent: ResponseActionExecuteOutputContent; 'data-test-subj'?: string; textSize?: 's' | 'xs'; + hideContext?: boolean; } +// Note: also used for RunScript command export const ExecuteActionHostResponseOutput = memo( - ({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs' }) => { + ({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs', hideContext }) => { const contextContent = useMemo( () => ( <> @@ -216,14 +219,16 @@ export const ExecuteActionHostResponseOutput = memo - - - + {!hideContext && ( + + + + )} {outputContent.stderr.length > 0 && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx new file mode 100644 index 0000000000000..e2ed358455027 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/run_script_action.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { ExecuteActionHostResponse } from '../../endpoint_execute_action'; +import { useSendRunScriptEndpoint } from '../../../hooks/response_actions/use_send_run_script_endpoint_request'; +import type { RunScriptActionRequestBody } from '../../../../../common/api/endpoint'; +import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter'; +import type { + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, +} from '../../../../../common/endpoint/types'; +import type { ActionRequestComponentProps } from '../types'; + +export const RunScriptActionResult = memo< + ActionRequestComponentProps< + { + Raw?: string; + HostPath?: string; + CloudFile?: string; + CommandLine?: string; + Timeout?: number; + comment?: string; + }, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + > +>(({ command, setStore, store, status, setStatus, ResultComponent }) => { + const actionCreator = useSendRunScriptEndpoint(); + const actionRequestBody = useMemo(() => { + const { endpointId, agentType } = command.commandDefinition?.meta ?? {}; + + if (!endpointId) { + return; + } + return { + agent_type: agentType, + endpoint_ids: [endpointId], + parameters: { + raw: command.args.args.Raw?.[0], + hostPath: command.args.args.HostPath?.[0], + cloudFile: command.args.args.CloudFile?.[0], + commandLine: command.args.args.CommandLine?.[0], + timeout: command.args.args.Timeout?.[0], + }, + comment: command.args.args?.comment?.[0], + }; + }, [command]); + + const { result, actionDetails: completedActionDetails } = useConsoleActionSubmitter< + RunScriptActionRequestBody, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + >({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator, + actionRequestBody, + dataTestSubj: 'runscript', + }); + + if (!completedActionDetails || !completedActionDetails.wasSuccessful) { + return result; + } + + return ( + + + + ); +}); +RunScriptActionResult.displayName = 'RunScriptActionResult'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 8c99186f69d93..22d138dea3586 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { RunScriptActionResult } from '../command_render_components/run_script_action'; import type { CommandArgDefinition } from '../../console/types'; import { isAgentTypeAndActionSupported } from '../../../../common/lib/endpoint'; import { getRbacControl } from '../../../../../common/endpoint/service/response_actions/utils'; @@ -531,14 +532,14 @@ export const getEndpointConsoleCommands = ({ aboutInfo: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, isSupported: doesEndpointSupportCommand('runscript'), }), - RenderComponent: () => null, + RenderComponent: RunScriptActionResult, meta: { agentType, endpointId: endpointAgentId, capabilities: endpointCapabilities, privileges: endpointPrivileges, }, - exampleUsage: `runscript --Raw="Get-ChildItem ." --CommandLine=""`, + exampleUsage: `runscript --Raw=\`\`\`Get-ChildItem .\`\`\` --CommandLine=""`, helpUsage: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.helpUsage, exampleInstruction: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.about, validate: capabilitiesAndPrivilegesValidator(agentType), diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx index cba1d0aee41b4..53f2c89ac84fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/action_log_expanded_tray.tsx @@ -23,6 +23,7 @@ import { isExecuteAction, isGetFileAction, isProcessesAction, + isRunScriptAction, isUploadAction, } from '../../../../../common/endpoint/service/response_actions/type_guards'; import { EndpointUploadActionResult } from '../../endpoint_upload_action_result'; @@ -209,6 +210,30 @@ const OutputContent = memo<{ ); } + if (isRunScriptAction(action)) { + return ( + + {action.agents.map((agentId) => ( +
+ {OUTPUT_MESSAGES.wasSuccessful(command)} + +
+ ))} +
+ ); + } + + // CrowdStrike Isolate/Release actions if (action.agentType === 'crowdstrike') { return <>{OUTPUT_MESSAGES.submittedSuccessfully(command)}; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts new file mode 100644 index 0000000000000..39eb3728162dc --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { useMutation as _useMutation } from '@tanstack/react-query'; +import type { AppContextTestRender } from '../../../common/mock/endpoint'; +import type { RenderHookResult } from '@testing-library/react'; +import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; +import { RUN_SCRIPT_ROUTE } from '../../../../common/endpoint/constants'; +import type { RunScriptActionRequestBody } from '../../../../common/api/endpoint'; +import type { + RunScriptRequestCustomOptions, + UseSendRunScriptRequestResult, +} from './use_send_run_script_endpoint_request'; +import { useSendRunScriptEndpoint } from './use_send_run_script_endpoint_request'; + +const useMutationMock = _useMutation as jest.Mock; + +jest.mock('@tanstack/react-query', () => { + const actualReactQueryModule = jest.requireActual('@tanstack/react-query'); + + return { + ...actualReactQueryModule, + useMutation: jest.fn((...args) => actualReactQueryModule.useMutation(...args)), + }; +}); + +const runScriptPayload: RunScriptActionRequestBody = { + endpoint_ids: ['test-endpoint-id'], + agent_type: 'crowdstrike', + parameters: { raw: 'ls' }, +}; + +describe('When using the `useSendRunScriptRequest()` hook', () => { + let customOptions: RunScriptRequestCustomOptions; + let http: AppContextTestRender['coreStart']['http']; + let apiMocks: ReturnType; + let renderHook: () => RenderHookResult< + UseSendRunScriptRequestResult, + RunScriptRequestCustomOptions + >; + + beforeEach(() => { + const testContext = createAppRootMockRenderer(); + + http = testContext.coreStart.http; + apiMocks = responseActionsHttpMocks(http); + customOptions = {}; + + renderHook = () => { + return testContext.renderHook(() => useSendRunScriptEndpoint(customOptions)); + }; + }); + + it('should call the `runScript` API with correct payload', async () => { + const { + result: { + current: { mutateAsync }, + }, + } = renderHook(); + await mutateAsync(runScriptPayload); + + expect(apiMocks.responseProvider.runscript).toHaveBeenCalledWith({ + body: JSON.stringify(runScriptPayload), + path: RUN_SCRIPT_ROUTE, + version: '2023-10-31', + }); + }); + + it('should allow custom options to be passed to ReactQuery', async () => { + customOptions.mutationKey = ['pqr-abc']; + customOptions.cacheTime = 10; + renderHook(); + + expect(useMutationMock).toHaveBeenCalledWith(expect.any(Function), customOptions); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts new file mode 100644 index 0000000000000..f4dd937fe3008 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/response_actions/use_send_run_script_endpoint_request.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { useMutation } from '@tanstack/react-query'; +import type { RunScriptActionRequestBody } from '../../../../common/api/endpoint'; +import { KibanaServices } from '../../../common/lib/kibana'; +import { RUN_SCRIPT_ROUTE } from '../../../../common/endpoint/constants'; +import type { ResponseActionApiResponse } from '../../../../common/endpoint/types'; + +export type RunScriptRequestCustomOptions = UseMutationOptions< + ResponseActionApiResponse, + IHttpFetchError, + RunScriptActionRequestBody +>; + +export type UseSendRunScriptRequestResult = UseMutationResult< + ResponseActionApiResponse, + IHttpFetchError, + RunScriptActionRequestBody +>; + +export const useSendRunScriptEndpoint = ( + options?: RunScriptRequestCustomOptions +): UseSendRunScriptRequestResult => { + return useMutation( + (runScriptActionReqBody) => { + return KibanaServices.get().http.post(RUN_SCRIPT_ROUTE, { + body: JSON.stringify(runScriptActionReqBody), + version: '2023-10-31', + }); + }, + options + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts index 256484f8d0e92..e8307895c3c4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/mocks/response_actions_http_mocks.ts @@ -17,6 +17,7 @@ import { GET_PROCESSES_ROUTE, ISOLATE_HOST_ROUTE_V2, KILL_PROCESS_ROUTE, + RUN_SCRIPT_ROUTE, SCAN_ROUTE, SUSPEND_PROCESS_ROUTE, UNISOLATE_HOST_ROUTE_V2, @@ -42,6 +43,8 @@ import type { ResponseActionScanParameters, ResponseActionUploadOutputContent, ResponseActionUploadParameters, + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters, } from '../../../common/endpoint/types'; export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{ @@ -73,6 +76,8 @@ export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{ >; scan: () => ActionDetailsApiResponse; + + runscript: () => ActionDetailsApiResponse; }>; export const responseActionsHttpMocks = httpHandlerMockFactory([ @@ -273,6 +278,25 @@ export const responseActionsHttpMocks = httpHandlerMockFactory => { + const generator = new EndpointActionGenerator('seed'); + const response = generator.generateActionDetails< + ResponseActionRunScriptOutputContent, + ResponseActionRunScriptParameters + >({ + command: 'runscript', + }); + return { data: response }; }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts index 1987a019f8887..d54df140b3b4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/response_actions.ts @@ -27,6 +27,7 @@ import type { ResponseActionGetFileParameters, EndpointActionResponseDataOutput, ResponseActionScanOutputContent, + ResponseActionRunScriptOutputContent, } from '../../../common/endpoint/types'; import { getFileDownloadId } from '../../../common/endpoint/service/response_actions/get_file_download_id'; import { @@ -138,6 +139,15 @@ export const sendEndpointActionResponse = async ( .content as unknown as ResponseActionExecuteOutputContent ).stderr = 'execute command timed out'; } + if ( + endpointResponse.EndpointActions.data.command === 'runscript' && + endpointResponse.EndpointActions.data.output + ) { + ( + endpointResponse.EndpointActions.data.output + .content as unknown as ResponseActionRunScriptOutputContent + ).stderr = 'runscript command timed out'; + } } await esClient.index({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts index 0c505b12c129d..25fcb30be4949 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/crowdstrike_actions_client.ts @@ -11,8 +11,13 @@ import { CROWDSTRIKE_CONNECTOR_ID, } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import type { CrowdstrikeBaseApiResponse } from '@kbn/stack-connectors-plugin/common/crowdstrike/types'; +import type { + CrowdstrikeBaseApiResponse, + CrowdStrikeExecuteRTRResponse, +} from '@kbn/stack-connectors-plugin/common/crowdstrike/types'; import { v4 as uuidv4 } from 'uuid'; + +import { mapParametersToCrowdStrikeArguments } from './utils'; import type { CrowdstrikeActionRequestCommonMeta } from '../../../../../../common/endpoint/types/crowdstrike'; import type { CommonResponseActionMethodOptions, @@ -305,15 +310,99 @@ export class CrowdstrikeActionsClient extends ResponseActionsClientImpl { ): Promise< ActionDetails > { - // TODO: just a placeholder for now - return Promise.resolve({ output: 'runscript', code: 200 }) as never as ActionDetails< - ResponseActionRunScriptOutputContent, - ResponseActionRunScriptParameters - >; + const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions = { + ...actionRequest, + ...this.getMethodOptions(options), + command: 'runscript', + }; + + let actionResponse: ActionTypeExecutorResult | undefined; + if (!reqIndexOptions.error) { + let error = (await this.validateRequest(reqIndexOptions)).error; + if (!error) { + if (!reqIndexOptions.actionId) { + reqIndexOptions.actionId = uuidv4(); + } + + try { + actionResponse = (await this.sendAction(SUB_ACTION.EXECUTE_ADMIN_RTR, { + actionParameters: { comment: this.buildExternalComment(reqIndexOptions) }, + command: mapParametersToCrowdStrikeArguments('runscript', actionRequest.parameters), + endpoint_ids: actionRequest.endpoint_ids, + })) as ActionTypeExecutorResult; + } catch (err) { + error = err; + } + } + + reqIndexOptions.error = error?.message; + + if (!this.options.isAutomated && error) { + throw error; + } + } + + const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions); + + // Ensure actionResponse is assigned before using it + if (actionResponse) { + await this.completeCrowdstrikeBatchAction(actionResponse, actionRequestDoc); + } + + await this.updateCases({ + command: reqIndexOptions.command, + caseIds: reqIndexOptions.case_ids, + alertIds: reqIndexOptions.alert_ids, + actionId: actionRequestDoc.EndpointActions.action_id, + hosts: actionRequest.endpoint_ids.map((agentId) => { + return { + hostId: agentId, + hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '', + }; + }), + comment: reqIndexOptions.comment, + }); + + return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id); + } + + private async completeCrowdstrikeBatchAction( + actionResponse: ActionTypeExecutorResult, + doc: LogsEndpointAction + ): Promise { + const agentId = doc.agent.id as string; + const stdout = actionResponse.data?.combined.resources[agentId].stdout || ''; + const stderr = actionResponse.data?.combined.resources[agentId].stderr || ''; + const error = actionResponse.data?.combined.resources[agentId].errors?.[0]; + const options = { + actionId: doc.EndpointActions.action_id, + agentId, + data: { + ...doc.EndpointActions.data, + output: { + content: { + stdout, + stderr, + code: '200', + }, + type: 'text' as const, + }, + }, + ...(error + ? { + error: { + code: error.code, + message: `Crowdstrike action failed: ${error.message}`, + }, + } + : {}), + }; + + await this.writeActionResponseToEndpointIndex(options); } private async completeCrowdstrikeAction( - actionResponse: ActionTypeExecutorResult | undefined, + actionResponse: ActionTypeExecutorResult, doc: LogsEndpointAction ): Promise { const options = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts new file mode 100644 index 0000000000000..f961c7a3d1d6b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { mapParametersToCrowdStrikeArguments } from './utils'; + +describe('mapParametersToCrowdStrikeArguments', () => { + it('returns command with single word parameter as is', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', { raw: 'echo Hello' }); + expect(result).toBe('runscript --Raw=```echo Hello```'); + }); + + it('wraps multi-word parameter in triple backticks', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', { + commandLine: 'echo Hello World', + }); + expect(result).toBe('runscript --CommandLine=```echo Hello World```'); + }); + + it('leaves parameter already wrapped in triple backticks unchanged', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', { + commandLine: '```echo Hello World```', + }); + expect(result).toBe('runscript --CommandLine=```echo Hello World```'); + }); + + it('trims spaces from parameter values', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', { raw: ' echo Hello ' }); + expect(result).toBe('runscript --Raw=```echo Hello```'); + }); + + it('handles multiple parameters correctly', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', { + raw: 'echo Hello', + commandLine: 'echo Hello World', + hostPath: '/home/user', + cloudFile: 'file.txt', + }); + expect(result).toBe( + 'runscript --Raw=```echo Hello``` --CommandLine=```echo Hello World``` --HostPath=/home/user --CloudFile=file.txt' + ); + }); + + it('returns command with no parameters correctly', () => { + const result = mapParametersToCrowdStrikeArguments('runscript', {}); + expect(result).toBe('runscript '); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts new file mode 100644 index 0000000000000..2ec2ec2bb0cf8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/crowdstrike/utils.ts @@ -0,0 +1,43 @@ +/* + * 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 { upperFirst } from 'lodash'; +import type { RunScriptActionRequestBody } from '../../../../../../common/api/endpoint'; + +export const mapParametersToCrowdStrikeArguments = ( + commandName: string, + parameters: RunScriptActionRequestBody['parameters'] +): string => { + // Map each parameter to the required syntax and join them with spaces + // In short: this function has to transform the parameters object into a string that can be used as a CS command + // One word commands eg. 'ls' can go as it is, but if there are more elements eg. 'ls -l', they have to be wrapped in triple backticks + const commandParts = Object.entries(parameters).map(([key, value]) => { + // Check and process the parameter value + let sanitizedValue; + if (typeof value === 'string') { + if (/^```.*```$/.test(value)) { + // If already wrapped in triple backticks, leave unchanged + sanitizedValue = value; + } else { + const strippedValue = value.trim(); // Remove spaces at the beginning and end + if (strippedValue.split(/\s+/).length === 1) { + // If it's a single element (no spaces), use it as-is + sanitizedValue = strippedValue; + } else { + // If it contains multiple elements (spaces), wrap in ``` + sanitizedValue = `\`\`\`${strippedValue}\`\`\``; + } + } + } else { + sanitizedValue = value; + } + return `--${upperFirst(key)}=${sanitizedValue}`; + }); + + // Combine the base command with the constructed parameters + return `${commandName} ${commandParts.join(' ')}`; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts index 6360ceba71cef..00b4774d9489c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/actions/clients/mocks.ts @@ -248,7 +248,7 @@ const createRunScriptOptionsMock = ( const options: RunScriptActionRequestBody = { ...createNoParamsResponseActionOptionsMock(), parameters: { - Raw: 'ls', + raw: 'ls', }, }; return merge(options, overrides); From 3e06d6a08842d1ce493e87692de5e9932b751146 Mon Sep 17 00:00:00 2001 From: Sergi Romeu Date: Tue, 17 Dec 2024 16:55:55 +0100 Subject: [PATCH 04/13] [ECO][Inventory] Temporary skip Cypress tests (#204578) ## Summary Related to #204558 Skipping cypress Inventory tests to avoid having the pipeline broken while we adapt them --- .../inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts | 3 ++- .../inventory/e2e/cypress/e2e/home.cy.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts index ac5fa17ecb08a..b303adc21d1fa 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts @@ -33,7 +33,8 @@ const verifyAlertsTableCount = (alertsCount: string) => { verifyNumber(cy.getByTestSubj('toolbar-alerts-count'), alertsCount); }; -describe('Alert count', () => { +// Temporary skipping those test, will be enabled in the future once we fix them https://github.com/elastic/kibana/issues/204558 +describe.skip('Alert count', () => { beforeEach(() => { cy.loginAsSuperUser(); diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index c2e7f1232e6aa..57ca19e429a1b 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -11,7 +11,8 @@ import { generateEntities, generateLogs, generateTraces } from './generate_data' const start = '2024-10-16T00:00:00.000Z'; const end = '2024-10-16T00:15:00.000Z'; -describe('Home page', () => { +// Temporary skipping those test, will be enabled in the future once we fix them https://github.com/elastic/kibana/issues/204558 +describe.skip('Home page', () => { beforeEach(() => { cy.loginAsSuperUser(); }); From c1bda1d50145f764956de9ed8557bd21f6ba136f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 17 Dec 2024 17:10:34 +0100 Subject: [PATCH 05/13] [HTTP] Default to `oldest` in dev (#203225) ## Summary Changes the default HTTP version resolution in dev to `oldest` (matches non-dev). The original intention was to guide developers to always sending a versioned header when interacting with Kibana. In practice, however, developers just set-and-forget the following configuration to get around this annoyance: ```yaml server.versioned.versionResolution: 'oldest' ``` ...undoing the original intention. Having this guidance does not justify the confusion/annoyance that this dev-only default creates and so this proposal simply removes it. To better guide developers we can consider other options like: make `version` required in core's HTTP client interface (lots of updates...), doing something in tests, adding docs, something else or any combo of these. Given the fact that developers generally discover this workaround and undo the originally intended guidance, I'm proposing not blocking on first having another approach in place. --- .../http/core-http-server-internal/src/http_config.test.ts | 4 ++-- .../core/http/core-http-server-internal/src/http_config.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/http/core-http-server-internal/src/http_config.test.ts b/packages/core/http/core-http-server-internal/src/http_config.test.ts index 4ba13489552bd..fa2fbe7ad9f36 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.test.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.test.ts @@ -517,9 +517,9 @@ describe('versioned', () => { ).toThrow(/failed validation/); }); - it('defaults version resolution "none" when in dev', () => { + it('defaults version resolution "oldest" when in dev', () => { expect(config.schema.validate({}, { dev: true })).toMatchObject({ - versioned: { versionResolution: 'none' }, + versioned: { versionResolution: 'oldest' }, }); }); }); diff --git a/packages/core/http/core-http-server-internal/src/http_config.ts b/packages/core/http/core-http-server-internal/src/http_config.ts index 11c1afc410331..4ba9bcb9e88be 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.ts @@ -213,7 +213,7 @@ const configSchema = schema.object( * Which handler resolution algo to use for public routes: "newest" or "oldest". * * @note Internal routes always require a version to be specified. - * @note in development we have an additional option "none" which is also the default in dev. + * @note in development we have an additional option "none". * This prevents any fallbacks and requires that a version specified. * Useful for ensuring that a given client always specifies a version. */ @@ -221,7 +221,7 @@ const configSchema = schema.object( schema.contextRef('dev'), true, schema.oneOf([schema.literal('newest'), schema.literal('oldest'), schema.literal('none')], { - defaultValue: 'none', + defaultValue: 'oldest', }), schema.oneOf([schema.literal('newest'), schema.literal('oldest')], { defaultValue: 'oldest', From 2c5544cfc87cfa11800e4ab687ab39ec445b2d74 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:17:05 +0000 Subject: [PATCH 06/13] [Cloud Security] Bug fix - show origin event's with primary color instead of danger (#204425) ## Summary Bug description: **Actual:** The node's color is red when exploring events through Explore or Timeline. **The expected** color of events is blue. Before: ![385007418-f0a6bd7e-dbc9-43ad-99b8-a07bcad85075](https://github.com/user-attachments/assets/7bf198f3-9a32-4d27-84db-3e97b5bf312b) After: https://github.com/user-attachments/assets/f1a10deb-65f5-43be-a351-6fca34f855cb https://github.com/user-attachments/assets/223534f4-09a2-4b41-85bc-c2195dd153ba **How to test this PR:** - Enable the feature flag `kibana.dev.yml`: ```yaml uiSettings.overrides.securitySolution:enableVisualizationsInFlyout: true xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled'] ``` - Load mocked data: ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` - Make sure you include data from Oct 13 2024. (in the video I use Last year) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../common/schema/graph/v1.ts | 4 +- .../graph_investigation.tsx | 45 ++- .../src/hooks/use_fetch_graph_data.test.tsx | 8 +- .../graph/src/hooks/use_fetch_graph_data.ts | 6 +- .../server/routes/graph/route.ts | 4 +- .../server/routes/graph/v1.ts | 53 ++-- .../left/components/graph_visualization.tsx | 23 +- .../left/tabs/visualize_tab.tsx | 4 +- .../graph_preview_container.test.tsx | 117 +++++++- .../components/graph_preview_container.tsx | 5 +- .../visualizations_section.test.tsx | 1 + .../components/visualizations_section.tsx | 4 +- .../shared/hooks/use_graph_preview.test.tsx | 259 ++++++++++++------ .../shared/hooks/use_graph_preview.ts | 16 +- .../apis/cloud_security_posture/graph.ts | 2 +- .../test/cloud_security_posture_api/config.ts | 10 +- .../routes/graph.ts | 63 ++++- .../security/cloud_security_posture/graph.ts | 4 +- 18 files changed, 483 insertions(+), 145 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts index 076c685aca5b9..5b1a48cf940b7 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -12,7 +12,9 @@ export const graphRequestSchema = schema.object({ nodesLimit: schema.maybe(schema.number()), showUnknownTarget: schema.maybe(schema.boolean()), query: schema.object({ - eventIds: schema.arrayOf(schema.string()), + originEventIds: schema.arrayOf( + schema.object({ id: schema.string(), isAlert: schema.boolean() }) + ), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx index 081b4ec28c6a5..bd57082ba4cb9 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx @@ -126,21 +126,46 @@ const useGraphPopovers = ( }; interface GraphInvestigationProps { - dataView: DataView; - eventIds: string[]; - timestamp: string | null; + /** + * The initial state to use for the graph investigation view. + */ + initialState: { + /** + * The data view to use for the graph investigation view. + */ + dataView: DataView; + + /** + * The origin events for the graph investigation view. + */ + originEventIds: Array<{ + /** + * The ID of the origin event. + */ + id: string; + + /** + * A flag indicating whether the origin event is an alert or not. + */ + isAlert: boolean; + }>; + + /** + * The initial timerange for the graph investigation view. + */ + timeRange: TimeRange; + }; } /** * Graph investigation view allows the user to expand nodes and view related entities. */ export const GraphInvestigation: React.FC = memo( - ({ dataView, eventIds, timestamp = new Date().toISOString() }: GraphInvestigationProps) => { + ({ + initialState: { dataView, originEventIds, timeRange: initialTimeRange }, + }: GraphInvestigationProps) => { const [searchFilters, setSearchFilters] = useState(() => []); - const [timeRange, setTimeRange] = useState({ - from: `${timestamp}||-30m`, - to: `${timestamp}||+30m`, - }); + const [timeRange, setTimeRange] = useState(initialTimeRange); const { services: { uiSettings }, @@ -153,7 +178,7 @@ export const GraphInvestigation: React.FC = memo( [...searchFilters], getEsQueryConfig(uiSettings as Parameters[0]) ), - [searchFilters, dataView, uiSettings] + [dataView, searchFilters, uiSettings] ); const { nodeExpandPopover, openPopoverCallback } = useGraphPopovers( @@ -166,7 +191,7 @@ export const GraphInvestigation: React.FC = memo( const { data, refresh, isFetching } = useFetchGraphData({ req: { query: { - eventIds, + originEventIds, esQuery: query, start: timeRange.from, end: timeRange.to, diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx index e494ff0957ecb..da5eaee9bfbf9 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx @@ -33,7 +33,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -52,7 +52,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -75,7 +75,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, @@ -98,7 +98,7 @@ describe('useFetchGraphData', () => { return useFetchGraphData({ req: { query: { - eventIds: [], + originEventIds: [], start: '2021-09-01T00:00:00.000Z', end: '2021-09-01T23:59:59.999Z', }, diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts index 74cca4693e801..477492a3bbb7b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts @@ -81,13 +81,13 @@ export const useFetchGraphData = ({ options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { const queryClient = useQueryClient(); - const { esQuery, eventIds, start, end } = req.query; + const { esQuery, originEventIds, start, end } = req.query; const { services: { http }, } = useKibana(); const QUERY_KEY = useMemo( - () => ['useFetchGraphData', eventIds, start, end, esQuery], - [end, esQuery, eventIds, start] + () => ['useFetchGraphData', originEventIds, start, end, esQuery], + [end, esQuery, originEventIds, start] ); const { isLoading, isError, data, isFetching } = useQuery( diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts index f9544b656f927..f655747815850 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -43,7 +43,7 @@ export const defineGraphRoute = (router: CspRouter) => const cspContext = await context.csp; const { nodesLimit, showUnknownTarget = false } = request.body; - const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; + const { originEventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; try { @@ -53,7 +53,7 @@ export const defineGraphRoute = (router: CspRouter) => esClient: cspContext.esClient, }, query: { - eventIds, + originEventIds, spaceId, start, end, diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts index b14a2ba3e06a9..d506bb856e766 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -33,7 +33,8 @@ interface GraphEdge { action: string; targetIds: string[] | string; eventOutcome: string; - isAlert: boolean; + isOrigin: boolean; + isOriginAlert: boolean; } interface LabelEdges { @@ -46,10 +47,15 @@ interface GraphContextServices { esClient: IScopedClusterClient; } +interface OriginEventId { + id: string; + isAlert: boolean; +} + interface GetGraphParams { services: GraphContextServices; query: { - eventIds: string[]; + originEventIds: OriginEventId[]; spaceId?: string; start: string | number; end: string | number; @@ -61,11 +67,13 @@ interface GetGraphParams { export const getGraph = async ({ services: { esClient, logger }, - query: { eventIds, spaceId = 'default', start, end, esQuery }, + query: { originEventIds, spaceId = 'default', start, end, esQuery }, showUnknownTarget, nodesLimit, }: GetGraphParams): Promise> => { - logger.trace(`Fetching graph for [eventIds: ${eventIds.join(', ')}] in [spaceId: ${spaceId}]`); + logger.trace( + `Fetching graph for [originEventIds: ${originEventIds.join(', ')}] in [spaceId: ${spaceId}]` + ); const results = await fetchGraph({ esClient, @@ -73,7 +81,7 @@ export const getGraph = async ({ logger, start, end, - eventIds, + originEventIds, esQuery, }); @@ -132,7 +140,7 @@ const fetchGraph = async ({ logger, start, end, - eventIds, + originEventIds, showUnknownTarget, esQuery, }: { @@ -140,15 +148,21 @@ const fetchGraph = async ({ logger: Logger; start: string | number; end: string | number; - eventIds: string[]; + originEventIds: OriginEventId[]; showUnknownTarget: boolean; esQuery?: EsQuery; }): Promise> => { + const originAlertIds = originEventIds.filter((originEventId) => originEventId.isAlert); const query = `from logs-* | WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL -| EVAL isAlert = ${ - eventIds.length > 0 - ? `event.id in (${eventIds.map((_id, idx) => `?al_id${idx}`).join(', ')})` +| EVAL isOrigin = ${ + originEventIds.length > 0 + ? `event.id in (${originEventIds.map((_id, idx) => `?og_id${idx}`).join(', ')})` + : 'false' + } +| EVAL isOriginAlert = isOrigin AND ${ + originAlertIds.length > 0 + ? `event.id in (${originAlertIds.map((_id, idx) => `?og_alrt_id${idx}`).join(', ')})` : 'false' } | STATS badge = COUNT(*), @@ -159,19 +173,26 @@ const fetchGraph = async ({ action = event.action, targetIds = target.entity.id, eventOutcome = event.outcome, - isAlert + isOrigin, + isOriginAlert | LIMIT 1000 -| SORT isAlert DESC`; +| SORT isOrigin DESC`; logger.trace(`Executing query [${query}]`); + const eventIds = originEventIds.map((originEventId) => originEventId.id); return await esClient.asCurrentUser.helpers .esql({ columnar: false, filter: buildDslFilter(eventIds, showUnknownTarget, start, end, esQuery), query, // @ts-ignore - types are not up to date - params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], + params: [ + ...originEventIds.map((originEventId, idx) => ({ [`og_id${idx}`]: originEventId.id })), + ...originEventIds + .filter((originEventId) => originEventId.isAlert) + .map((originEventId, idx) => ({ [`og_alrt_id${idx}`]: originEventId.id })), + ], }) .toRecords(); }; @@ -238,7 +259,7 @@ const createNodes = (records: GraphEdge[], context: Omit { const dataView = useGetScopedSourcererDataView({ sourcererScope: SourcererScopeName.default, }); - const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); - const { eventIds, timestamp } = useGraphPreview({ + const { getFieldsData, dataAsNestedObject, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); + const { + eventIds, + timestamp = new Date().toISOString(), + isAlert, + } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); + const originEventIds = eventIds.map((id) => ({ id, isAlert })); + return (
{ > {dataView && ( }> - + )}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 89e00e06e3a49..1a8e5906e247e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -88,7 +88,8 @@ const graphVisualizationButton: EuiButtonGroupOptionProps = { * Visualize view displayed in the document details expandable flyout left section */ export const VisualizeTab = memo(() => { - const { scopeId, getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); + const { scopeId, getFieldsData, dataAsNestedObject, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); const { openPreviewPanel } = useExpandableFlyoutApi(); const panels = useExpandableFlyoutState(); const [activeVisualizationId, setActiveVisualizationId] = useState( @@ -123,6 +124,7 @@ export const VisualizeTab = memo(() => { const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index c805f2a3c67a7..9965c5300e71f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -86,6 +86,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -111,7 +112,109 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should render component for alert', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: DEFAULT_NODES, edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: ['eventId'], + isAlert: true, + hasGraphRepresentation: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + originEventIds: [{ id: 'eventId', isAlert: true }], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should render component for event', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: DEFAULT_NODES, edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: ['eventId'], + isAlert: false, + hasGraphRepresentation: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + originEventIds: [{ id: 'eventId', isAlert: false }], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -136,6 +239,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -164,7 +268,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -189,6 +293,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -217,7 +322,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -243,6 +348,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: true, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -268,7 +374,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, @@ -293,6 +399,7 @@ describe('', () => { timestamp, eventIds: [], hasGraphRepresentation: false, + isAlert: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -320,7 +427,7 @@ describe('', () => { expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ req: { query: { - eventIds: [], + originEventIds: [], start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 90a0218778549..b4626f93e823d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -31,6 +31,7 @@ export const GraphPreviewContainer: React.FC = () => { scopeId, isPreview, isPreviewMode, + dataFormattedForFieldBrowser, } = useDocumentDetailsContext(); const [visualizationInFlyoutEnabled] = useUiSetting$( @@ -49,16 +50,18 @@ export const GraphPreviewContainer: React.FC = () => { eventIds, timestamp = new Date().toISOString(), hasGraphRepresentation, + isAlert, } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { query: { - eventIds, + originEventIds: eventIds.map((id) => ({ id, isAlert })), start: `${timestamp}||-30m`, end: `${timestamp}||+30m`, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 6fb4d5d30b897..dc3b1f00e0d50 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -106,6 +106,7 @@ describe('', () => { }); mockUseGraphPreview.mockReturnValue({ hasGraphRepresentation: true, + eventIds: [], }); mockUseFetchGraphData.mockReturnValue({ isLoading: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index 23bea1f8fecdd..467171cd49f2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -28,7 +28,8 @@ const KEY = 'visualizations'; */ export const VisualizationsSection = memo(() => { const expanded = useExpandSection({ title: KEY, defaultValue: false }); - const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); + const { dataAsNestedObject, getFieldsData, dataFormattedForFieldBrowser } = + useDocumentDetailsContext(); const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING @@ -42,6 +43,7 @@ export const VisualizationsSection = memo(() => { const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, + dataFormattedForFieldBrowser, }); const shouldShowGraphPreview = diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx index 453f897d4e188..cf1ee82078395 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx @@ -5,15 +5,18 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { renderHook } from '@testing-library/react'; -import type { UseGraphPreviewParams, UseGraphPreviewResult } from './use_graph_preview'; +import type { UseGraphPreviewParams } from './use_graph_preview'; import { useGraphPreview } from './use_graph_preview'; import type { GetFieldsData } from './use_get_fields_data'; import { mockFieldData } from '../mocks/mock_get_fields_data'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; -const mockGetFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { +const alertMockGetFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return 'alertId'; + } else if (field === 'kibana.alert.original_event.id') { return 'eventId'; } else if (field === 'actor.entity.id') { return 'actorId'; @@ -24,18 +27,36 @@ const mockGetFieldsData: GetFieldsData = (field: string) => { return mockFieldData[field]; }; -describe('useGraphPreview', () => { - let hookResult: RenderHookResult; +const alertMockDataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; + +const eventMockGetFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return; + } else if (field === 'kibana.alert.original_event.id') { + return; + } else if (field === 'event.id') { + return 'eventId'; + } else if (field === 'actor.entity.id') { + return 'actorId'; + } else if (field === 'target.entity.id') { + return 'targetId'; + } + return mockFieldData[field]; +}; + +const eventMockDataFormattedForFieldBrowser: TimelineEventsDetailsItem[] = []; + +describe('useGraphPreview', () => { it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { if (field === 'actor.entity.id') { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -44,37 +65,41 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual([]); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: [], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when missing event.action`, () => { - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { - getFieldsData: mockGetFieldsData, + getFieldsData: alertMockGetFieldsData, ecsData: { _id: 'id', }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(undefined); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: undefined, + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when missing target`, () => { @@ -82,26 +107,28 @@ describe('useGraphPreview', () => { if (field === 'target.entity.id') { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { _id: 'id', }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual([]); - expect(action).toEqual(undefined); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: undefined, + targetIds: [], + isAlert: true, + }); }); it(`should return false when missing original_event.id`, () => { @@ -110,10 +137,10 @@ describe('useGraphPreview', () => { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -122,17 +149,19 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual([]); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: [], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); it(`should return false when timestamp is missing`, () => { @@ -141,10 +170,10 @@ describe('useGraphPreview', () => { return; } - return mockGetFieldsData(field); + return alertMockGetFieldsData(field); }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -153,45 +182,53 @@ describe('useGraphPreview', () => { action: ['action'], }, }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(false); - expect(timestamp).toEqual(null); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: null, + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); }); - it(`should return true when alert is has graph preview`, () => { - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + it(`should return true when event has graph graph preview`, () => { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { - getFieldsData: mockGetFieldsData, + getFieldsData: eventMockGetFieldsData, ecsData: { _id: 'id', event: { action: ['action'], }, }, + dataFormattedForFieldBrowser: eventMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(true); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['eventId']); - expect(actorIds).toEqual(['actorId']); - expect(targetIds).toEqual(['targetId']); - expect(action).toEqual(['action']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: false, + }); }); - it(`should return true when alert is has graph preview with multiple values`, () => { + it(`should return true when event has graph preview with multiple values`, () => { const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { + if (field === 'kibana.alert.uuid') { + return; + } else if (field === 'kibana.alert.original_event.id') { + return; + } else if (field === 'event.id') { return ['id1', 'id2']; } else if (field === 'actor.entity.id') { return ['actorId1', 'actorId2']; @@ -202,7 +239,7 @@ describe('useGraphPreview', () => { return mockFieldData[field]; }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { getFieldsData, ecsData: { @@ -211,16 +248,82 @@ describe('useGraphPreview', () => { action: ['action1', 'action2'], }, }, + dataFormattedForFieldBrowser: eventMockDataFormattedForFieldBrowser, }, }); - const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = - hookResult.result.current; - expect(hasGraphRepresentation).toEqual(true); - expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); - expect(eventIds).toEqual(['id1', 'id2']); - expect(actorIds).toEqual(['actorId1', 'actorId2']); - expect(action).toEqual(['action1', 'action2']); - expect(targetIds).toEqual(['targetId1', 'targetId2']); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['id1', 'id2'], + actorIds: ['actorId1', 'actorId2'], + action: ['action1', 'action2'], + targetIds: ['targetId1', 'targetId2'], + isAlert: false, + }); + }); + + it(`should return true when alert has graph preview`, () => { + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData: alertMockGetFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); + }); + + it(`should return true when alert has graph preview with multiple values`, () => { + const getFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.uuid') { + return 'alertId'; + } else if (field === 'kibana.alert.original_event.id') { + return ['id1', 'id2']; + } else if (field === 'actor.entity.id') { + return ['actorId1', 'actorId2']; + } else if (field === 'target.entity.id') { + return ['targetId1', 'targetId2']; + } + + return mockFieldData[field]; + }; + + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action1', 'action2'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: true, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['id1', 'id2'], + actorIds: ['actorId1', 'actorId2'], + action: ['action1', 'action2'], + targetIds: ['targetId1', 'targetId2'], + isAlert: true, + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts index 48233afab02df..8f05b87844fb2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { get } from 'lodash/fp'; import type { GetFieldsData } from './use_get_fields_data'; import { getField, getFieldArray } from '../utils'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; export interface UseGraphPreviewParams { /** @@ -20,6 +22,11 @@ export interface UseGraphPreviewParams { * An object with top level fields from the ECS object */ ecsData: Ecs; + + /** + * An array of field objects with category and value + */ + dataFormattedForFieldBrowser: TimelineEventsDetailsItem[]; } /** * Interface for the result of the useGraphPreview hook @@ -54,6 +61,11 @@ export interface UseGraphPreviewResult { * Boolean indicating if the event is has a graph representation (contains event ids, actor ids and action) */ hasGraphRepresentation: boolean; + + /** + * Boolean indicating if the event is an alert or not + */ + isAlert: boolean; } /** @@ -62,6 +74,7 @@ export interface UseGraphPreviewResult { export const useGraphPreview = ({ getFieldsData, ecsData, + dataFormattedForFieldBrowser, }: UseGraphPreviewParams): UseGraphPreviewResult => { const timestamp = getField(getFieldsData('@timestamp')); const originalEventId = getFieldsData('kibana.alert.original_event.id'); @@ -77,6 +90,7 @@ export const useGraphPreview = ({ actorIds.length > 0 && eventIds.length > 0 && targetIds.length > 0; + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation }; + return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation, isAlert }; }; diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts index 4ff483bff343d..4823c500a3588 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts @@ -40,7 +40,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 404 when feature flag is not toggled', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 4e0ecd1f26e43..212abc50fc9ae 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -5,7 +5,7 @@ * 2.0. */ import { resolve } from 'path'; -import type { FtrConfigProviderContext } from '@kbn/test'; +import { getKibanaCliLoggers, type FtrConfigProviderContext } from '@kbn/test'; import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -21,6 +21,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--logging.loggers=${JSON.stringify([ + ...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')), + { + name: 'plugins.cloudSecurityPosture', + level: 'all', + appenders: ['default'], + }, + ])}`, /** * Package version is fixed (not latest) so FTR won't suddenly break when package is changed. * diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index 08adf73839ea2..e2be81a7d40e5 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -48,7 +48,7 @@ export default function (providerContext: FtrProviderContext) { supertestWithoutAuth, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -88,7 +88,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 400 when missing `esQuery` field is not of type bool', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', esQuery: { @@ -102,7 +102,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 400 with unsupported `esQuery`', async () => { await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', esQuery: { @@ -122,7 +122,7 @@ export default function (providerContext: FtrProviderContext) { it('should return an empty graph / should return 200 when missing `esQuery` field', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -136,7 +136,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -177,7 +177,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -204,10 +204,40 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should return a graph with nodes and edges by origin event', async () => { + const response = await postGraph(supertest, { + query: { + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: false }], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); + expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + 'primary', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + 'primary', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + it('color of alert of failed event should be danger', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['failed-event'], + originEventIds: [{ id: 'failed-event', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -237,7 +267,7 @@ export default function (providerContext: FtrProviderContext) { it('color of event of failed event should be warning', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -279,7 +309,7 @@ export default function (providerContext: FtrProviderContext) { it('2 grouped events, 1 failed, 1 success', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -327,7 +357,10 @@ export default function (providerContext: FtrProviderContext) { it('should support more than 1 eventIds', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678', 'failed-event'], + originEventIds: [ + { id: 'kabcd1234efgh5678', isAlert: true }, + { id: 'failed-event', isAlert: true }, + ], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', }, @@ -357,7 +390,7 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert and actor', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -402,7 +435,7 @@ export default function (providerContext: FtrProviderContext) { it('should filter unknown targets', async () => { const response = await postGraph(supertest, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -428,7 +461,7 @@ export default function (providerContext: FtrProviderContext) { const response = await postGraph(supertest, { showUnknownTarget: true, query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -454,7 +487,7 @@ export default function (providerContext: FtrProviderContext) { const response = await postGraph(supertest, { nodesLimit: 1, query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { @@ -480,7 +513,7 @@ export default function (providerContext: FtrProviderContext) { it('should support date math', async () => { const response = await postGraph(supertest, { query: { - eventIds: ['kabcd1234efgh5678'], + originEventIds: [{ id: 'kabcd1234efgh5678', isAlert: true }], start: '2024-09-01T12:30:00.000Z||-30m', end: '2024-09-01T12:30:00.000Z||+30m', }, diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts index aaccdd0e9a41c..2cbe40e945492 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts @@ -49,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { it('should return an empty graph', async () => { const response = await postGraph(supertestViewer, { query: { - eventIds: [], + originEventIds: [], start: 'now-1d/d', end: 'now/d', }, @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertestViewer, { query: { - eventIds: [], + originEventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', esQuery: { From 1b52848c5b041bdd0500945cc4a1b98ef6360f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Lidue=C3=B1a?= Date: Tue, 17 Dec 2024 17:33:40 +0100 Subject: [PATCH 07/13] [Observability] Update required field from 'score' to 'scores' (#203584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #203739 ## Summary fix error: `“Error scoring documents: Tool call arguments for score were invalid”` [Observability] Update required field from 'score' to 'scores' in scoreSuggestions schema --- .../server/utils/recall/score_suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts index 7d1a19463cceb..b57e5928ce0ba 100644 --- a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts +++ b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/utils/recall/score_suggestions.ts @@ -102,7 +102,7 @@ export async function scoreSuggestions({ type: 'string', }, }, - required: ['score'], + required: ['scores'], } as const, }; From cc34e97de4d2d8a5509eec5e4f9ffcb7338ed54a Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 17 Dec 2024 17:45:16 +0100 Subject: [PATCH 08/13] Fix code scanning alert no. 494: Incomplete string escaping or encoding (#204528) Fixes [https://github.com/elastic/kibana/security/code-scanning/494](https://github.com/elastic/kibana/security/code-scanning/494) Fixes #1311 Escape first backslash occurrences and all single quotes in the `filter` string. --- .../public/components/rule_condition_chart/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts b/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts index 30663d02cda72..775da2fc1c805 100644 --- a/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts +++ b/x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/helpers.ts @@ -18,7 +18,7 @@ export const getLensOperationFromRuleMetric = (metric: GenericMetric): LensOpera const { aggType, field, filter = '' } = metric; let operation: string = aggType; const operationArgs: string[] = []; - const escapedFilter = filter.replace(/'/g, "\\'"); + const escapedFilter = filter.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); if (aggType === Aggregators.RATE) { return { From 02b65bd457293a71b730cf5ffbb376e637e2a7ff Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 17 Dec 2024 11:50:33 -0500 Subject: [PATCH 09/13] [Fleet] Remove adopted showDevToolRequest feature flag (#204586) --- .../fleet/common/experimental_features.ts | 1 - .../hooks/devtools_request.tsx | 8 +--- .../components/settings/index.tsx | 42 +++++++++---------- .../components/create_agent_policy.tsx | 36 +++++++--------- 4 files changed, 36 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 1c42bf3ab2afe..1cf35952904aa 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -10,7 +10,6 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const _allowedExperimentalValues = { createPackagePolicyMultiPageLayout: true, packageVerification: true, - showDevtoolsRequest: true, diagnosticFileUploadEnabled: true, displayAgentMetrics: true, showIntegrationsSubcategories: true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx index 91ca089f2ffe3..81f2304efee99 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; -import { ExperimentalFeaturesService } from '../../../../../services'; import { generateCreatePackagePolicyDevToolsRequest, generateCreateAgentPolicyDevToolsRequest, @@ -39,12 +38,7 @@ export function useDevToolsRequest({ packageInfo?: PackageInfo; packagePolicyId?: string; }) { - const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } = - ExperimentalFeaturesService.get(); - - const showDevtoolsRequest = - !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') && - isShowDevtoolRequestExperimentEnabled; + const showDevtoolsRequest = !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? ''); const [devtoolRequest, devtoolRequestDescription] = useMemo(() => { if (selectedPolicyTab === SelectedPolicyTab.NEW) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 50baac84ff4a0..efb7066b0d5e0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -40,7 +40,6 @@ import { ConfirmDeployAgentPolicyModal, } from '../../../components'; import { DevtoolsRequestFlyoutButton } from '../../../../../components'; -import { ExperimentalFeaturesService } from '../../../../../services'; import { generateUpdateAgentPolicyDevToolsRequest } from '../../../services'; import { UNKNOWN_SPACE } from '../../../../../../../../common/constants'; @@ -155,7 +154,6 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( setIsLoading(false); }; - const { showDevtoolsRequest } = ExperimentalFeaturesService.get(); const devtoolRequest = useMemo( () => generateUpdateAgentPolicyDevToolsRequest( @@ -235,28 +233,26 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( />
- {showDevtoolsRequest ? ( - - 0 || - hasAdvancedSettingsErrors || - hasInvalidSpaceError + + 0 || + hasAdvancedSettingsErrors || + hasInvalidSpaceError + } + btnProps={{ + color: 'text', + }} + description={i18n.translate( + 'xpack.fleet.editAgentPolicy.devtoolsRequestDescription', + { + defaultMessage: 'This Kibana request updates an agent policy.', } - btnProps={{ - color: 'text', - }} - description={i18n.translate( - 'xpack.fleet.editAgentPolicy.devtoolsRequestDescription', - { - defaultMessage: 'This Kibana request updates an agent policy.', - } - )} - request={devtoolRequest} - /> - - ) : null} + )} + request={devtoolRequest} + /> + = ({ /> ); - const { showDevtoolsRequest } = ExperimentalFeaturesService.get(); const agentPolicyContent = useMemo( () => generateCreateAgentPolicyDevToolsRequest(agentPolicy, withSysMonitoring), [agentPolicy, withSysMonitoring] @@ -128,25 +126,23 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ - {showDevtoolsRequest ? ( - - 0 || - hasAdvancedSettingsErrors || - hasInvalidSpaceError + + 0 || + hasAdvancedSettingsErrors || + hasInvalidSpaceError + } + description={i18n.translate( + 'xpack.fleet.createAgentPolicy.devtoolsRequestDescription', + { + defaultMessage: 'This Kibana request creates a new agent policy.', } - description={i18n.translate( - 'xpack.fleet.createAgentPolicy.devtoolsRequestDescription', - { - defaultMessage: 'This Kibana request creates a new agent policy.', - } - )} - request={agentPolicyContent} - /> - - ) : null} + )} + request={agentPolicyContent} + /> + Date: Tue, 17 Dec 2024 17:13:02 +0000 Subject: [PATCH 10/13] Fix share modal copy embed code (#204584) ## Summary Closes https://github.com/elastic/kibana/issues/204312 This PR removes an unnecessary function, that causes an infinite render loop for visualisations. ## After https://github.com/user-attachments/assets/259d238b-c1cf-4d74-bfca-1a6440d0f5cd ## How to test - From the left side nav, click the `Visualize Library` menu item, attempt to create a legacy visualisation, any one of choice and attempt sharing said created visualisation clicking the copy embed code should not result in any error but rather copy the embed code with the visual feedback. --- .../tabs/embed/embed_content.test.tsx | 7 +--- .../components/tabs/embed/embed_content.tsx | 6 ---- .../public/components/tabs/embed/index.tsx | 34 +------------------ 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx b/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx index ee9fdf01588df..edbb9ebb47995 100644 --- a/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx +++ b/src/plugins/share/public/components/tabs/embed/embed_content.test.tsx @@ -18,12 +18,7 @@ describe('Share modal embed content tab', () => { beforeEach(() => { component = mountWithIntl( - jest.fn()} - shareableUrl="/home#/" - /> + ); }); diff --git a/src/plugins/share/public/components/tabs/embed/embed_content.tsx b/src/plugins/share/public/components/tabs/embed/embed_content.tsx index 5a9163097c8dc..a3bc6048e64e5 100644 --- a/src/plugins/share/public/components/tabs/embed/embed_content.tsx +++ b/src/plugins/share/public/components/tabs/embed/embed_content.tsx @@ -34,7 +34,6 @@ type EmbedProps = Pick< | 'objectType' | 'isDirty' > & { - setIsNotSaved: () => void; objectConfig?: ShareContextObjectTypeConfig; }; @@ -55,7 +54,6 @@ export const EmbedContent = ({ shareableUrl, objectType, objectConfig = {}, - setIsNotSaved, isDirty, }: EmbedProps) => { const isMounted = useMountedState(); @@ -67,10 +65,6 @@ export const EmbedContent = ({ const [anonymousAccessParameters] = useState(null); const [usePublicUrl] = useState(false); - useEffect(() => { - if (objectType !== 'dashboard') setIsNotSaved(); - }, [url, setIsNotSaved, objectType]); - const makeUrlEmbeddable = useCallback((tempUrl: string): string => { const embedParam = '?embed=true'; const urlHasQueryString = tempUrl.indexOf('?') !== -1; diff --git a/src/plugins/share/public/components/tabs/embed/index.tsx b/src/plugins/share/public/components/tabs/embed/index.tsx index 44d61833268cd..3c74ce3fe0b13 100644 --- a/src/plugins/share/public/components/tabs/embed/index.tsx +++ b/src/plugins/share/public/components/tabs/embed/index.tsx @@ -8,35 +8,13 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useCallback } from 'react'; +import React from 'react'; import { type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal'; import { EmbedContent } from './embed_content'; import { useShareTabsContext } from '../../context'; -const EMBED_TAB_ACTIONS = { - SET_EMBED_URL: 'SET_EMBED_URL', - SET_IS_NOT_SAVED: 'SET_IS_NOT_SAVED', -}; - type IEmbedTab = IModalTabDeclaration<{ url: string; isNotSaved: boolean }>; -const embedTabReducer: IEmbedTab['reducer'] = (state = { url: '', isNotSaved: false }, action) => { - switch (action.type) { - case EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED: - return { - ...state, - isNotSaved: action.payload, - }; - case EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED: - return { - ...state, - isNotSaved: action.payload, - }; - default: - return state; - } -}; - const EmbedTabContent: NonNullable = ({ state, dispatch }) => { const { embedUrlParamExtensions, @@ -47,13 +25,6 @@ const EmbedTabContent: NonNullable = ({ state, dispatch }) isDirty, } = useShareTabsContext()!; - const setIsNotSaved = useCallback(() => { - dispatch({ - type: EMBED_TAB_ACTIONS.SET_IS_NOT_SAVED, - payload: objectType === 'dashboard' ? isDirty : false, - }); - }, [dispatch, objectType, isDirty]); - return ( = ({ state, dispatch }) shareableUrl, objectType, objectConfig: objectTypeMeta?.config?.embed, - isNotSaved: state?.isNotSaved, - setIsNotSaved, isDirty, }} /> @@ -75,6 +44,5 @@ export const embedTab: IEmbedTab = { name: i18n.translate('share.contextMenu.embedCodeTab', { defaultMessage: 'Embed', }), - reducer: embedTabReducer, content: EmbedTabContent, }; From 15dc9c7b45f0007558b30d6ea83ecc41737f3653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Bl=C3=A1zquez?= Date: Tue, 17 Dec 2024 19:39:34 +0100 Subject: [PATCH 11/13] Implement feature flag to toggle Asset Inventory plugin (#203844) ## Summary Closes https://github.com/elastic/kibana/issues/201705. Reuse security_solution's config to control visibility of Asset Inventory plugin. ### Definition of done - [x] Use the existing `xpack.securitySolution.enableExperimental` configuration to add a new feature flag - Done in `x-pack/plugins/security_solution/common/experimental_features.ts` - [x] Implement the feature flag `xpack.securitySolution.enableExperimental: ['assetInventoryStoreEnabled']`. - This can only be done locally in `kibana.dev.yml`, but I can add it to `kibana.yml` instead. - [x] Update any relevant documentation to explain how to enable the feature. - Updated plugin's `README` file. ### How to test In your [kibana.yml](https://github.com/elastic/kibana/blob/main/config/kibana.yml) file, add the following entry at the bottom: ``` xpack.securitySolution.enableExperimental: ['assetInventoryStoreEnabled'] ``` ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../solutions/security/plugins/asset_inventory/README.md | 8 ++++++++ .../security_solution/common/experimental_features.ts | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/asset_inventory/README.md b/x-pack/solutions/security/plugins/asset_inventory/README.md index e1dd9d4ac8900..313a45cec6f76 100755 --- a/x-pack/solutions/security/plugins/asset_inventory/README.md +++ b/x-pack/solutions/security/plugins/asset_inventory/README.md @@ -8,6 +8,14 @@ Centralized asset inventory experience within the Elastic Security solution. A c See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. +### Feature flag + +First, enable the `assetInventoryStoreEnabled` experimental feature flag by adding the following to your `kibana.dev.yml`: + +```yml +xpack.securitySolution.enableExperimental: ['assetInventoryStoreEnabled'] +``` + ## Testing For general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 428a48cf4b7be..b09e8cae5391c 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -266,8 +266,12 @@ export const allowedExperimentalValues = Object.freeze({ /** * Enables CrowdStrike's RunScript RTR command */ - crowdstrikeRunScriptEnabled: false, + + /** + * Enables the Asset Inventory feature + */ + assetInventoryStoreEnabled: false, }); type ExperimentalConfigKeys = Array; From 62c99f73fdbf2649b75baf4d1fa3092850068c36 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Tue, 17 Dec 2024 20:09:37 +0100 Subject: [PATCH 12/13] [ECO][Inventory V2] Turn off entityCentricExperience feature flag by default (#204278) Closes #204238 ## Summary This PR turns off the`entityCentricExperience` feature flag by default | Before | After | | ------ | ------ | | image| ![image](https://github.com/user-attachments/assets/20906756-3209-4b16-a44b-eacba6180874)| | ![image](https://github.com/user-attachments/assets/37e1b6bc-1395-4f32-8e5b-1ddf4afd93b6)| image | | ![image](https://github.com/user-attachments/assets/16b78af2-f9fc-4263-87cd-9dfa978410da)| image | | ![image](https://github.com/user-attachments/assets/90d1b791-8f7f-40c2-a73b-3263cfc17a7f) | ![image](https://github.com/user-attachments/assets/fd0417b0-5257-4bca-993f-51c08be47c14)| ## Testing The `entityCentricExperience` feature flag should be turned off by default and the inventory menu item should not be visible. The inventory menu item should be visible if the flag is manually turned on. https://github.com/user-attachments/assets/7bee8ccb-bb29-451e-8a64-22325d9d8477 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Romeu --- .../use_entity_centric_experience_setting.tsx | 2 +- .../use_entity_centric_experience_setting.tsx | 2 +- .../entities/get_data_stream_types.test.ts | 18 +++++++++--------- .../routes/entities/get_data_stream_types.ts | 6 +++--- .../infra/server/routes/entities/index.ts | 4 ++-- .../cypress/e2e/alert_count/alert_count.cy.ts | 7 +++++++ .../inventory/e2e/cypress/e2e/home.cy.ts | 19 +++++++++++++++++++ .../inventory/e2e/cypress/support/commands.ts | 14 ++++++++++++++ .../inventory/e2e/cypress/support/types.d.ts | 1 + .../inventory/public/plugin.ts | 2 +- .../observability/server/ui_settings.ts | 2 +- 11 files changed, 59 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/public/hooks/use_entity_centric_experience_setting.tsx b/x-pack/plugins/observability_solution/apm/public/hooks/use_entity_centric_experience_setting.tsx index 32e68c40788fc..e5290bb25dbba 100644 --- a/x-pack/plugins/observability_solution/apm/public/hooks/use_entity_centric_experience_setting.tsx +++ b/x-pack/plugins/observability_solution/apm/public/hooks/use_entity_centric_experience_setting.tsx @@ -13,7 +13,7 @@ export function useEntityCentricExperienceSetting() { const isEntityCentricExperienceEnabled = core.uiSettings.get( entityCentricExperience, - true + false ); return { isEntityCentricExperienceEnabled }; diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx b/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx index 1cbc4680405e4..1ac6eed88f2ad 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx @@ -11,7 +11,7 @@ import { useKibanaContextForPlugin } from './use_kibana'; export function useEntityCentricExperienceSetting() { const { uiSettings } = useKibanaContextForPlugin().services; - const isEntityCentricExperienceEnabled = uiSettings.get(entityCentricExperience, true); + const isEntityCentricExperienceEnabled = uiSettings.get(entityCentricExperience); return { isEntityCentricExperienceEnabled }; } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts index 75048ac22a6a8..4a1466b698c78 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts @@ -35,13 +35,13 @@ describe('getDataStreamTypes', () => { jest.clearAllMocks(); }); - it('should return only metrics when entityCentriExperienceEnabled is false and hasMetricsData is true', async () => { + it('should return only metrics when entityCentricExperienceEnabled is false and hasMetricsData is true', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(true); const params = { entityId: 'entity123', entityType: 'host' as EntityType, - entityCentriExperienceEnabled: false, + entityCentricExperienceEnabled: false, infraMetricsClient, obsEsClient, entityManagerClient, @@ -58,13 +58,13 @@ describe('getDataStreamTypes', () => { }); }); - it('should return an empty array when entityCentriExperienceEnabled is false and hasMetricsData is false', async () => { + it('should return an empty array when entityCentricExperienceEnabled is false and hasMetricsData is false', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(false); const params = { entityId: 'entity123', entityType: 'container' as EntityType, - entityCentriExperienceEnabled: false, + entityCentricExperienceEnabled: false, infraMetricsClient, obsEsClient, entityManagerClient, @@ -75,7 +75,7 @@ describe('getDataStreamTypes', () => { expect(result).toEqual([]); }); - it('should return metrics and entity source_data_stream types when entityCentriExperienceEnabled is true and has entity data', async () => { + it('should return metrics and entity source_data_stream types when entityCentricExperienceEnabled is true and has entity data', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(true); (getLatestEntity as jest.Mock).mockResolvedValue({ sourceDataStreamType: ['logs', 'metrics'], @@ -84,7 +84,7 @@ describe('getDataStreamTypes', () => { const params = { entityId: 'entity123', entityType: 'host' as EntityType, - entityCentriExperienceEnabled: true, + entityCentricExperienceEnabled: true, infraMetricsClient, obsEsClient, entityManagerClient, @@ -104,14 +104,14 @@ describe('getDataStreamTypes', () => { }); }); - it('should return only metrics when entityCentriExperienceEnabled is true but entity data is undefined', async () => { + it('should return only metrics when entityCentricExperienceEnabled is true but entity data is undefined', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(true); (getLatestEntity as jest.Mock).mockResolvedValue(undefined); const params = { entityId: 'entity123', entityType: 'host' as EntityType, - entityCentriExperienceEnabled: true, + entityCentricExperienceEnabled: true, infraMetricsClient, obsEsClient, entityManagerClient, @@ -131,7 +131,7 @@ describe('getDataStreamTypes', () => { const params = { entityId: 'entity123', entityType: 'host' as EntityType, - entityCentriExperienceEnabled: true, + entityCentricExperienceEnabled: true, infraMetricsClient, obsEsClient, entityManagerClient, diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts index 4a949de4d0ed7..d1cd99be31f9f 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts @@ -18,7 +18,7 @@ import { getLatestEntity } from './get_latest_entity'; interface Params { entityId: string; entityType: 'host' | 'container'; - entityCentriExperienceEnabled: boolean; + entityCentricExperienceEnabled: boolean; infraMetricsClient: InfraMetricsClient; obsEsClient: ObservabilityElasticsearchClient; entityManagerClient: EntityClient; @@ -26,7 +26,7 @@ interface Params { } export async function getDataStreamTypes({ - entityCentriExperienceEnabled, + entityCentricExperienceEnabled, entityId, entityManagerClient, entityType, @@ -42,7 +42,7 @@ export async function getDataStreamTypes({ const sourceDataStreams = new Set(hasMetricsData ? [EntityDataStreamType.METRICS] : []); - if (!entityCentriExperienceEnabled) { + if (!entityCentricExperienceEnabled) { return Array.from(sourceDataStreams); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts index 4056faba9427e..9c5c5c4527fee 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts @@ -54,13 +54,13 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => { plugin: `@kbn/${METRICS_APP_ID}-plugin`, }); - const entityCentriExperienceEnabled = await coreContext.uiSettings.client.get( + const entityCentricExperienceEnabled = await coreContext.uiSettings.client.get( entityCentricExperience ); try { const sourceDataStreamTypes = await getDataStreamTypes({ - entityCentriExperienceEnabled, + entityCentricExperienceEnabled, entityId, entityManagerClient, entityType, diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts index b303adc21d1fa..525828de10b18 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/alert_count/alert_count.cy.ts @@ -38,6 +38,10 @@ describe.skip('Alert count', () => { beforeEach(() => { cy.loginAsSuperUser(); + cy.updateAdvancedSettings({ + 'observability:entityCentricExperience': true, + }); + cy.intercept('GET', '/internal/entities/managed/enablement', { fixture: 'eem_enabled.json', }).as('getEEMStatus'); @@ -50,6 +54,9 @@ describe.skip('Alert count', () => { }); afterEach(() => { + cy.updateAdvancedSettings({ + 'observability:entityCentricExperience': false, + }); entitiesSynthtrace.clean(); cleanEntityAlerts(); }); diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 57ca19e429a1b..e86c673c2a6b7 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -15,6 +15,25 @@ const end = '2024-10-16T00:15:00.000Z'; describe.skip('Home page', () => { beforeEach(() => { cy.loginAsSuperUser(); + cy.updateAdvancedSettings({ + 'observability:entityCentricExperience': true, + }); + }); + + afterEach(() => { + cy.updateAdvancedSettings({ + 'observability:entityCentricExperience': false, + }); + }); + + describe('When the entityCentricExperience FF is disabled', () => { + it('Shows 404', () => { + cy.updateAdvancedSettings({ + 'observability:entityCentricExperience': false, + }); + cy.visitKibana('/app/inventory'); + cy.contains('Application Not Found'); + }); }); describe('When EEM is disabled', () => { diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/commands.ts index c3462f8b6ff18..9a260519c7608 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/commands.ts @@ -55,3 +55,17 @@ Cypress.Commands.add( ); } ); + +Cypress.Commands.add('updateAdvancedSettings', (settings: Record) => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + cy.request({ + log: false, + method: 'POST', + url: `${kibanaUrl}/internal/kibana/settings`, + body: { changes: settings }, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'elastic', pass: 'changeme' }, + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/types.d.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/types.d.ts index c51b20c3b9904..8e49da093612f 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/support/types.d.ts @@ -13,6 +13,7 @@ declare namespace Cypress { password: string; }): Cypress.Chainable>; getByTestSubj(selector: string): Chainable>; + updateAdvancedSettings(settings: Record): void; visitKibana(url: string): void; } } diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts index 109123859d4ca..0d9309d3694fb 100644 --- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts @@ -57,7 +57,7 @@ export class InventoryPlugin const inventoryAPIClient = createCallInventoryAPI(coreSetup); const isEntityCentricExperienceSettingEnabled = coreSetup.uiSettings.get( 'observability:entityCentricExperience', - true + false ); this.telemetry.setup({ diff --git a/x-pack/solutions/observability/plugins/observability/server/ui_settings.ts b/x-pack/solutions/observability/plugins/observability/server/ui_settings.ts index e5695a3c9034d..a68ae28b267fc 100644 --- a/x-pack/solutions/observability/plugins/observability/server/ui_settings.ts +++ b/x-pack/solutions/observability/plugins/observability/server/ui_settings.ts @@ -325,7 +325,7 @@ export const uiSettings: Record = { }, }), schema: schema.boolean(), - value: true, + value: false, requiresPageReload: true, type: 'boolean', }, From e529a899bcfbb28e2d490642fc4d29ee522debf8 Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Tue, 17 Dec 2024 21:02:56 +0100 Subject: [PATCH 13/13] [CI] skip flaky integration test (#204645) ## Summary Skips suite "reindex & waitForReindexTask" - Errors are reported here: https://github.com/elastic/kibana/issues/167273 - Original issue, I believe: https://github.com/elastic/kibana/issues/166190 - PR that recently unskipped the test: https://github.com/elastic/kibana/pull/204561 --- .../migrations/group3/actions/actions_test_suite.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts index 41a194f5f1e5d..c6995c6e2d4ca 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/actions/actions_test_suite.ts @@ -772,7 +772,9 @@ export const runActionTestSuite = ({ // Reindex doesn't return any errors on its own, so we have to test // together with waitForReindexTask - describe('reindex & waitForReindexTask', () => { + // Flaky: https://github.com/elastic/kibana/issues/166190 + // Reported here: https://github.com/elastic/kibana/issues/167273 + describe.skip('reindex & waitForReindexTask', () => { it('resolves right when reindex succeeds without reindex script', async () => { const res = (await reindex({ client,