From 4d46ad6f1301e1ff12fcfbb33094446a736299fe Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 19 Dec 2024 16:33:25 +0100 Subject: [PATCH 1/2] WIP custom scripts --- .../get_agent_status_route.test.ts | 54 ++++++ .../get_custom_scripts_route.ts | 25 +++ .../common/endpoint/constants.ts | 3 + .../custom_script_selector.tsx | 144 ++++++++++++++ .../lib/console_commands_definition.ts | 2 + .../custom_scripts/use_get_custom_scripts.ts | 49 +++++ .../agent_status_handler.test.ts | 183 ++++++++++++++++++ .../custom_scripts/custom_scripts_handler.ts | 114 +++++++++++ .../endpoint/routes/custom_scripts/index.ts | 17 ++ .../actions/clients/crowdstrike/utils.ts | 7 +- .../server/endpoint/services/index.ts | 1 + .../crowdstrike_custom_scrtipts_client.ts | 55 ++++++ .../clients/get_custom_scripts_client.ts | 36 ++++ .../services/scripts/clients/index.ts | 10 + .../clients/lib/base_custom_scripts_client.ts | 35 ++++ .../services/scripts/clients/lib/errors.ts | 51 +++++ .../services/scripts/clients/lib/types.ts | 12 ++ .../server/endpoint/services/scripts/index.ts | 8 + .../security_solution/server/plugin.ts | 2 + 19 files changed, 806 insertions(+), 2 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_agent_status_route.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_custom_scripts_route.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/hooks/custom_scripts/use_get_custom_scripts.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/agent_status_handler.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/custom_scripts_handler.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/crowdstrike/crowdstrike_custom_scrtipts_client.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/get_custom_scripts_client.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/base_custom_scripts_client.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/errors.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/types.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/index.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_agent_status_route.test.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_agent_status_route.test.ts new file mode 100644 index 0000000000000..a0091e52b7652 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_agent_status_route.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EndpointAgentStatusRequestSchema } from './get_agent_status_route'; + +describe('Agent status api route schema', () => { + it('should optionally accept `agentType`', () => { + expect(() => + EndpointAgentStatusRequestSchema.query.validate({ + agentIds: '1', + }) + ).not.toThrow(); + }); + + it('should error if unknown `agentType` is used', () => { + expect(() => + EndpointAgentStatusRequestSchema.query.validate({ + agentIds: '1', + agentType: 'foo', + }) + ).toThrow(/\[agentType]: types that failed validation/); + }); + + it.each([ + ['string with spaces only', { agentIds: ' ' }], + ['empty string', { agentIds: '' }], + ['array with empty strings', { agentIds: [' ', ''] }], + ['agentIds not defined', {}], + ['agentIds is empty array', { agentIds: [] }], + [ + 'more than 50 agentIds', + { agentIds: Array.from({ length: 51 }, () => Math.random().toString(32)) }, + ], + ])('should error if %s are used for `agentIds`', (_, validateOptions) => { + expect(() => EndpointAgentStatusRequestSchema.query.validate(validateOptions)).toThrow( + /\[agentIds]:/ + ); + }); + + it.each([ + ['single string value', 'one'], + ['array of strings', ['one', 'two']], + ])('should accept %s of `agentIds`', (_, agentIdsValue) => { + expect(() => + EndpointAgentStatusRequestSchema.query.validate({ + agentIds: agentIdsValue, + }) + ).not.toThrow(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_custom_scripts_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_custom_scripts_route.ts new file mode 100644 index 0000000000000..83896dd034b44 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/endpoint/custom_scripts/get_custom_scripts_route.ts @@ -0,0 +1,25 @@ +/* + * 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 { schema, type TypeOf } from '@kbn/config-schema'; +import { AgentTypeSchemaLiteral } from '..'; + +export const CustomScriptsRequestSchema = { + query: schema.object({ + agentType: schema.maybe( + schema.oneOf( + // @ts-expect-error TS2769: No overload matches this call + AgentTypeSchemaLiteral, + { + defaultValue: 'endpoint', + } + ) + ), + }), +}; + +export type CustomScriptsRequestQueryParams = TypeOf; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts index c08dc5b811f84..7fb0a8fbedf55 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/constants.ts @@ -109,6 +109,9 @@ export const ACTION_STATE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/state`; /** Endpoint Agent Routes */ export const AGENT_STATUS_ROUTE = `/internal${BASE_ENDPOINT_ROUTE}/agent_status`; +/** Custom Scripts Routes */ +export const CUSTOM_SCRIPTS_ROUTE = `/internal${BASE_ENDPOINT_ROUTE}/custom_scripts`; + export const failedFleetActionErrorCode = '424'; export const ENDPOINT_DEFAULT_PAGE = 0; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx new file mode 100644 index 0000000000000..4f56a0dedbb4f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useMemo, useState } from 'react'; +import { + EuiPopover, + EuiSuperSelect, + EuiText, + EuiFlexGroup, + EuiFlexItem, + htmlIdGenerator, +} from '@elastic/eui'; +import type { EuiFilePickerProps } from '@elastic/eui/src/components/form/file_picker/file_picker'; +import { i18n } from '@kbn/i18n'; +import { useGetCustomScripts } from '../../hooks/custom_scripts/use_get_custom_scripts'; +import type { CommandArgumentValueSelectorProps } from '../console/types'; + +const INITIAL_DISPLAY_LABEL = i18n.translate( + 'xpack.securitySolution.consoleArgumentSelectors.customScriptSelector.initialDisplayLabel', + { defaultMessage: 'Click to select script' } +); + +const OPEN_FILE_PICKER_LABEL = i18n.translate( + 'xpack.securitySolution.consoleArgumentSelectors.customScriptSelector.filePickerButtonLabel', + { defaultMessage: 'Open scripts picker' } +); + +const NO_FILE_SELECTED = i18n.translate( + 'xpack.securitySolution.consoleArgumentSelectors.customScriptSelector.noFileSelected', + { defaultMessage: 'No script selected' } +); + +interface ArgumentFileSelectorState { + isPopoverOpen: boolean; +} + +/** + * A Console Argument Selector component that enables the user to select a file from the local machine + */ +export const CustomScriptSelector = memo< + CommandArgumentValueSelectorProps +>(({ value, valueText, onChange, store: _store }) => { + const state = useMemo(() => { + return _store ?? { isPopoverOpen: true }; + }, [_store]); + + const setIsPopoverOpen = useCallback( + (newValue: boolean) => { + onChange({ + value, + valueText, + store: { + ...state, + isPopoverOpen: newValue, + }, + }); + }, + [onChange, state, value, valueText] + ); + console.log({ store: _store }); + const { data = [] } = useGetCustomScripts('crowdstrike'); + + const scriptsOptions = useMemo(() => { + return data.map((script) => ({ + value: script.name, + inputDisplay: script.name, + dropdownDisplay: ( + <> + {script.name} + +

{script.description}

+
+ + ), + })); + }, [data]); + + const filePickerUUID = useMemo(() => { + return htmlIdGenerator('console')(); + }, []); + const handleOpenPopover = useCallback(() => { + setIsPopoverOpen(true); + }, [setIsPopoverOpen]); + + const handleClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, [setIsPopoverOpen]); + + const [selectedScript, setSelectedScript] = useState(''); + const handleScriptSelection = useCallback>( + (selectedScript) => { + console.log({ selectedScript }); + + // Get only the first file selected + setSelectedScript(selectedScript); + onChange({ + value: selectedScript ?? undefined, + valueText: selectedScript || '', + store: { + ...state, + isPopoverOpen: false, + }, + }); + // TODO fix focus back to the input and not Back > button + }, + [onChange, state, selectorRef] + ); + + return ( +
+ + +
+ {valueText || INITIAL_DISPLAY_LABEL} +
+
+ + } + > + {state.isPopoverOpen && ( + + )} +
+
+ ); +}); +CustomScriptSelector.displayName = 'ArgumentFileSelector'; 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 22d138dea3586..0a403db6ea6bc 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 { CustomScriptSelector } from '../../console_argument_selectors/custom_script_selector'; import { RunScriptActionResult } from '../command_render_components/run_script_action'; import type { CommandArgDefinition } from '../../console/types'; import { isAgentTypeAndActionSupported } from '../../../../common/lib/endpoint'; @@ -558,6 +559,7 @@ export const getEndpointConsoleCommands = ({ about: CROWDSTRIKE_CONSOLE_COMMANDS.runscript.args.cloudFile.about, mustHaveValue: 'non-empty-string', exclusiveOr: true, + SelectorComponent: CustomScriptSelector, }, CommandLine: { required: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/hooks/custom_scripts/use_get_custom_scripts.ts b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/custom_scripts/use_get_custom_scripts.ts new file mode 100644 index 0000000000000..e53e5e0d076b4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/hooks/custom_scripts/use_get_custom_scripts.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; +import { CUSTOM_SCRIPTS_ROUTE } from '../../../../common/endpoint/constants'; +import { useHttp } from '../../../common/lib/kibana'; + +interface ErrorType { + statusCode: number; + message: string; + meta: ActionTypeExecutorResult; +} + +/** + * Retrieve the status of a supported host's agent type + * @param agentType + * @param options + */ +export const useGetCustomScripts = ( + agentType: ResponseActionAgentType, + options: any = {} +): UseQueryResult> => { + const http = useHttp(); + + return useQuery>({ + queryKey: ['get-agent-status', agentType], + // refetchInterval: DEFAULT_POLL_INTERVAL, + ...options, + queryFn: () => { + return http + .get(CUSTOM_SCRIPTS_ROUTE, { + version: '1', + query: { + agentType, + }, + }) + .then((response) => response.data); + }, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/agent_status_handler.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/agent_status_handler.test.ts new file mode 100644 index 0000000000000..6ea890cdf716e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/agent_status_handler.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { HttpApiTestSetupMock } from '../../mocks'; +import { createHttpApiTestSetupMock } from '../../mocks'; +import { sentinelOneMock } from '../../services/actions/clients/sentinelone/mocks'; +import { registerAgentStatusRoute } from './agent_status_handler'; +import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants'; +import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; +import type { EndpointAgentStatusRequestQueryParams } from '../../../../common/api/endpoint/agent/get_agent_status_route'; +import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; +import { RESPONSE_ACTION_AGENT_TYPE } from '../../../../common/endpoint/service/response_actions/constants'; +import type { ExperimentalFeatures } from '../../../../common'; +import { agentServiceMocks as mockAgentService } from '../../services/agent/mocks'; +import { getAgentStatusClient as _getAgentStatusClient } from '../../services'; +import type { DeepMutable } from '../../../../common/endpoint/types'; + +jest.mock('../../services', () => { + const realModule = jest.requireActual('../../services'); + + return { + ...realModule, + getAgentStatusClient: jest.fn((agentType: ResponseActionAgentType) => { + return mockAgentService.createClient(agentType); + }), + }; +}); + +const getAgentStatusClientMock = _getAgentStatusClient as jest.Mock; + +describe('Agent Status API route handler', () => { + let apiTestSetup: HttpApiTestSetupMock>; + let httpRequestMock: ReturnType< + HttpApiTestSetupMock< + never, + DeepMutable + >['createRequestMock'] + >; + let httpHandlerContextMock: HttpApiTestSetupMock< + never, + EndpointAgentStatusRequestQueryParams + >['httpHandlerContextMock']; + let httpResponseMock: HttpApiTestSetupMock< + never, + EndpointAgentStatusRequestQueryParams + >['httpResponseMock']; + + beforeEach(async () => { + apiTestSetup = createHttpApiTestSetupMock(); + ({ httpHandlerContextMock, httpResponseMock } = apiTestSetup); + + httpRequestMock = apiTestSetup.createRequestMock({ + query: { agentType: 'sentinel_one', agentIds: ['one', 'two'] }, + }); + + ( + (await apiTestSetup.httpHandlerContextMock.actions).getActionsClient as jest.Mock + ).mockReturnValue(sentinelOneMock.createConnectorActionsClient()); + + apiTestSetup.endpointAppContextMock.experimentalFeatures = { + ...apiTestSetup.endpointAppContextMock.experimentalFeatures, + responseActionsSentinelOneV1Enabled: true, + responseActionsCrowdstrikeManualHostIsolationEnabled: true, + }; + + registerAgentStatusRoute(apiTestSetup.routerMock, apiTestSetup.endpointAppContextMock); + }); + + it.each` + agentType | featureFlag + ${'sentinel_one'} | ${'responseActionsSentinelOneV1Enabled'} + ${'crowdstrike'} | ${'responseActionsCrowdstrikeManualHostIsolationEnabled'} + `( + 'should error if the $agentType feature flag ($featureFlag) is turned off', + async ({ + agentType, + featureFlag, + }: { + agentType: ResponseActionAgentType; + featureFlag: keyof ExperimentalFeatures; + }) => { + apiTestSetup.endpointAppContextMock.experimentalFeatures = { + ...apiTestSetup.endpointAppContextMock.experimentalFeatures, + [featureFlag]: false, + }; + httpRequestMock.query.agentType = agentType; + + await apiTestSetup + .getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1') + .routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + + expect(httpResponseMock.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: expect.any(CustomHttpRequestError), + }); + } + ); + + it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept agent type of %s', async (agentType) => { + httpRequestMock.query.agentType = agentType; + await apiTestSetup + .getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1') + .routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + + expect(httpResponseMock.ok).toHaveBeenCalled(); + expect(getAgentStatusClientMock).toHaveBeenCalledWith(agentType, { + esClient: (await httpHandlerContextMock.core).elasticsearch.client.asInternalUser, + soClient: + apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient(), + connectorActionsClient: (await httpHandlerContextMock.actions).getActionsClient(), + endpointService: apiTestSetup.endpointAppContextMock.service, + }); + }); + + it('should return status code 200 with expected payload', async () => { + await apiTestSetup + .getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1') + .routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + + expect(httpResponseMock.ok).toHaveBeenCalledWith({ + body: { + data: { + one: { + agentType: 'sentinel_one', + found: true, + agentId: 'one', + isolated: false, + lastSeen: expect.any(String), + pendingActions: {}, + status: 'healthy', + }, + two: { + agentType: 'sentinel_one', + found: true, + agentId: 'two', + isolated: false, + lastSeen: expect.any(String), + pendingActions: {}, + status: 'healthy', + }, + }, + }, + }); + }); + + it('should NOT use space ID in creating SO client when feature is disabled', async () => { + await apiTestSetup + .getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1') + .routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + + expect(httpResponseMock.ok).toHaveBeenCalled(); + expect( + apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient + ).toHaveBeenCalledWith({ + spaceId: undefined, + }); + }); + + it('should use a scoped SO client when spaces awareness feature is enabled', async () => { + // @ts-expect-error write to readonly property + apiTestSetup.endpointAppContextMock.service.experimentalFeatures.endpointManagementSpaceAwarenessEnabled = + true; + + ((await httpHandlerContextMock.securitySolution).getSpaceId as jest.Mock).mockReturnValue( + 'foo' + ); + + await apiTestSetup + .getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1') + .routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock); + + expect(httpResponseMock.ok).toHaveBeenCalled(); + expect( + apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient + ).toHaveBeenCalledWith({ + spaceId: 'foo', + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/custom_scripts_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/custom_scripts_handler.ts new file mode 100644 index 0000000000000..12276d0d02696 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/custom_scripts_handler.ts @@ -0,0 +1,114 @@ +/* + * 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 { RequestHandler } from '@kbn/core/server'; +import { getCustomScriptsClient } from '../../services/scripts/clients/get_custom_scripts_client'; +import { CustomScriptsRequestSchema } from '../../../../common/api/endpoint/custom_scripts/get_custom_scripts_route'; +import { errorHandler } from '../error_handler'; +import type { EndpointAgentStatusRequestQueryParams } from '../../../../common/api/endpoint/agent/get_agent_status_route'; +import { CUSTOM_SCRIPTS_ROUTE } from '../../../../common/endpoint/constants'; +import type { + SecuritySolutionPluginRouter, + SecuritySolutionRequestHandlerContext, +} from '../../../types'; +import type { EndpointAppContext } from '../../types'; +import { withEndpointAuthz } from '../with_endpoint_authz'; +import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; + +export const registerCustomScriptsRoute = ( + router: SecuritySolutionPluginRouter, + endpointContext: EndpointAppContext +) => { + router.versioned + .get({ + access: 'internal', + path: CUSTOM_SCRIPTS_ROUTE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, + }) + .addVersion( + { + version: '1', + validate: { + request: CustomScriptsRequestSchema, + }, + }, + withEndpointAuthz( + { all: ['canReadSecuritySolution'] }, + endpointContext.logFactory.get('customScriptsRoute'), + getAgentStatusRouteHandler(endpointContext) + ) + ); +}; + +export const getAgentStatusRouteHandler = ( + endpointContext: EndpointAppContext +): RequestHandler< + never, + EndpointAgentStatusRequestQueryParams, + unknown, + SecuritySolutionRequestHandlerContext +> => { + const logger = endpointContext.logFactory.get('customScriptsRoute'); + + return async (context, request, response) => { + const { agentType = 'endpoint', agentIds: _agentIds } = request.query; + const agentIds = Array.isArray(_agentIds) ? _agentIds : [_agentIds]; + + logger.debug( + `Retrieving status for: agentType [${agentType}], agentIds: [${agentIds.join(', ')}]` + ); + + // Note: because our API schemas are defined as module static variables (as opposed to a + // `getter` function), we need to include this additional validation here, since + // `agent_type` is included in the schema independent of the feature flag + if ( + (agentType === 'sentinel_one' && + !endpointContext.experimentalFeatures.responseActionsSentinelOneV1Enabled) || + (agentType === 'crowdstrike' && + !endpointContext.experimentalFeatures.crowdstrikeRunScriptEnabled) + ) { + return errorHandler( + logger, + response, + new CustomHttpRequestError(`[request query.agent_type]: feature is disabled`, 400) + ); + } + + try { + const [securitySolutionPlugin, corePlugin, actionsPlugin] = await Promise.all([ + context.securitySolution, + context.core, + context.actions, + ]); + const esClient = corePlugin.elasticsearch.client.asInternalUser; + const spaceId = endpointContext.service.experimentalFeatures + .endpointManagementSpaceAwarenessEnabled + ? securitySolutionPlugin.getSpaceId() + : undefined; + const soClient = endpointContext.service.savedObjects.createInternalScopedSoClient({ + spaceId, + }); + const connectorActionsClient = actionsPlugin.getActionsClient(); + const customScriptsClient = getCustomScriptsClient(agentType, { + esClient, + soClient, + connectorActionsClient, + endpointService: endpointContext.service, + }); + const data = await customScriptsClient.getCustomScripts(); + + return response.ok({ body: { data } }); + } catch (e) { + return errorHandler(logger, response, e); + } + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/index.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/index.ts new file mode 100644 index 0000000000000..784593b16a23a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/routes/custom_scripts/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { registerCustomScriptsRoute } from './custom_scripts_handler'; +import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { EndpointAppContext } from '../../types'; + +export const registerCustomScriptsRoutes = ( + router: SecuritySolutionPluginRouter, + endpointContext: EndpointAppContext +) => { + registerCustomScriptsRoute(router, endpointContext); +}; 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 index 2ec2ec2bb0cf8..3ae35806e9c8a 100644 --- 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 @@ -28,8 +28,11 @@ export const mapParametersToCrowdStrikeArguments = ( // 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}\`\`\``; + // If parameter is raw and it contains multiple elements (spaces), wrap in ``` + const wrappedValue = + key === 'raw' ? `\`\`\`${strippedValue}\`\`\`` : `'${strippedValue}'`; + + sanitizedValue = wrappedValue; } } } else { diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/index.ts index 53c49d315ffac..61ae87c2f41d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/index.ts @@ -8,6 +8,7 @@ export * from './artifacts'; export * from './actions'; export * from './agent'; +export * from './scripts'; export * from './artifacts_exception_list'; export * from './workflow_insights'; export type { FeatureKeys } from './feature_usage'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/crowdstrike/crowdstrike_custom_scrtipts_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/crowdstrike/crowdstrike_custom_scrtipts_client.ts new file mode 100644 index 0000000000000..474284bf65521 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/crowdstrike/crowdstrike_custom_scrtipts_client.ts @@ -0,0 +1,55 @@ +/* + * 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 { + CROWDSTRIKE_CONNECTOR_ID, + SUB_ACTION, +} from '@kbn/stack-connectors-plugin/common/crowdstrike/constants'; +import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import type { CrowdstrikeGetAgentOnlineStatusResponse } from '@kbn/stack-connectors-plugin/common/crowdstrike/types'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import { CustomScriptsClientError } from '../lib/errors'; +import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants'; +import { NormalizedExternalConnectorClient } from '../../..'; +import { CustomScriptsClient } from '../lib/base_custom_scripts_client'; + +export class CrowdstrikeCustomScriptsClient extends CustomScriptsClient { + protected readonly agentType: ResponseActionAgentType = 'crowdstrike'; + + private async getCustomScriptsFromConnectorAction(agentIds: string[]) { + const connectorActions = new NormalizedExternalConnectorClient( + this.options.connectorActionsClient as ActionsClient, + this.log + ); + connectorActions.setup(CROWDSTRIKE_CONNECTOR_ID); + + const customScriptsResponse = (await connectorActions.execute({ + params: { + subAction: SUB_ACTION.GET_RTR_CLOUD_SCRIPTS, + subActionParams: {}, + }, + })) as ActionTypeExecutorResult; + + return customScriptsResponse.data?.resources; + } + + async getCustomScripts(agentIds: string[]): Promise { + try { + const customScripts = await this.getCustomScriptsFromConnectorAction(agentIds); + + return customScripts; + } catch (err) { + const error = new CustomScriptsClientError( + `Failed to fetch crowdstrike agent status for agentIds: [${agentIds}], failed with: ${err.message}`, + 500, + err + ); + this.log.error(error); + throw error; + } + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/get_custom_scripts_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/get_custom_scripts_client.ts new file mode 100644 index 0000000000000..c13282390a146 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/get_custom_scripts_client.ts @@ -0,0 +1,36 @@ +/* + * 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 { UnsupportedAgentTypeError } from './lib/errors'; +import type { CustomScriptsClientOptions } from './lib/base_custom_scripts_client'; +import type { CustomScriptsClientInterface } from './lib/types'; +import { CrowdstrikeCustomScriptsClient } from './crowdstrike/crowdstrike_custom_scrtipts_client'; +import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants'; + +/** + * Retrieve a agent status client for an agent type + * @param agentType + * @param constructorOptions + * + */ +export const getCustomScriptsClient = ( + agentType: ResponseActionAgentType, + constructorOptions: CustomScriptsClientOptions +): CustomScriptsClientInterface => { + switch (agentType) { + // case 'endpoint': + // return new EndpointCustomScriptsClient(constructorOptions); + // case 'sentinel_one': + // return new SentinelOneCustomScriptsClient(constructorOptions); + case 'crowdstrike': + return new CrowdstrikeCustomScriptsClient(constructorOptions); + default: + throw new UnsupportedAgentTypeError( + `Agent type [${agentType}] does not support agent status` + ); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/index.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/index.ts new file mode 100644 index 0000000000000..25eeec2bee2e2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './crowdstrike/crowdstrike_custom_scrtipts_client'; + +export * from './lib/types'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/base_custom_scripts_client.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/base_custom_scripts_client.ts new file mode 100644 index 0000000000000..e3bf3a469abf1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/base_custom_scripts_client.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { Logger } from '@kbn/logging'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import { CustomScriptsNotSupportedError } from './errors'; +import type { AgentStatusRecords } from '../../../../../../common/endpoint/types/agents'; +import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants'; +import type { EndpointAppContextService } from '../../../../endpoint_app_context_services'; +import type { CustomScriptsClientInterface } from './types'; + +export interface CustomScriptsClientOptions { + endpointService: EndpointAppContextService; + esClient: ElasticsearchClient; + soClient: SavedObjectsClientContract; + connectorActionsClient?: ActionsClient; +} + +export abstract class CustomScriptsClient implements CustomScriptsClientInterface { + protected readonly log: Logger; + protected abstract readonly agentType: ResponseActionAgentType; + + constructor(protected readonly options: CustomScriptsClientOptions) { + this.log = options.endpointService.createLogger(this.constructor.name ?? 'CustomScriptsClient'); + } + + public async getCustomScripts(agentIds: string[]): Promise { + throw new CustomScriptsNotSupportedError(agentIds, this.agentType); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/errors.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/errors.ts new file mode 100644 index 0000000000000..19429466172cf --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/errors.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. + */ + +/* eslint-disable max-classes-per-file */ + +import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants'; +import { CustomHttpRequestError } from '../../../../../utils/custom_http_request_error'; +import { stringify } from '../../../../utils/stringify'; + +/** + * Errors associated with Agent Status clients + */ +export class CustomScriptsClientError extends CustomHttpRequestError { + toJSON() { + return { + message: this.message, + statusCode: this.statusCode, + meta: this.meta, + stack: this.stack, + }; + } + + toString() { + return stringify(this.toJSON()); + } +} + +export class UnsupportedAgentTypeError extends CustomScriptsClientError { + constructor(message: string, statusCode = 501, meta?: unknown) { + super(message, statusCode, meta); + } +} + +export class CustomScriptsNotSupportedError extends CustomScriptsClientError { + constructor( + agentIds: string[], + agentType: ResponseActionAgentType, + statusCode: number = 405, + meta?: unknown + ) { + super( + `Customs scripts are not available for ${`[agentIds: ${agentIds} and agentType: ${agentType}]`} not supported`, + statusCode, + meta + ); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/types.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/types.ts new file mode 100644 index 0000000000000..51d4f46fe6594 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/clients/lib/types.ts @@ -0,0 +1,12 @@ +/* + * 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 { AgentStatusRecords } from '../../../../../../common/endpoint/types'; + +export interface CustomScriptsClientInterface { + getCustomScripts: (agentIds: string[]) => Promise; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/index.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/index.ts new file mode 100644 index 0000000000000..3b6ed23908dda --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/services/scripts/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './clients'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index d8fa5c61ee7f3..37603eefc7135 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -19,6 +19,7 @@ import type { ILicense } from '@kbn/licensing-plugin/server'; import type { NewPackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common'; +import { registerCustomScriptsRoutes } from './endpoint/routes/custom_scripts'; import { ensureIndicesExistsForPolicies } from './endpoint/migrations/ensure_indices_exists_for_policies'; import { CompleteExternalResponseActionsTask } from './endpoint/lib/response_actions'; import { registerAgentRoutes } from './endpoint/routes/agent'; @@ -417,6 +418,7 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.encryptedSavedObjects?.canEncrypt === true ); registerAgentRoutes(router, this.endpointContext); + registerCustomScriptsRoutes(router, this.endpointContext); if (plugins.alerting != null) { const ruleNotificationType = legacyRulesNotificationRuleType({ logger }); From f01f3c4bdcea9605e19337d6df57ded593c72f6c Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 19 Dec 2024 16:34:41 +0100 Subject: [PATCH 2/2] fix --- .../console_argument_selectors/custom_script_selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx index 4f56a0dedbb4f..3d65b3f5e977b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/components/console_argument_selectors/custom_script_selector.tsx @@ -107,7 +107,7 @@ export const CustomScriptSelector = memo< }); // TODO fix focus back to the input and not Back > button }, - [onChange, state, selectorRef] + [onChange, state] ); return (