diff --git a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts index dff47cf86ab6f..1bd7c2eece1da 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts @@ -132,7 +132,7 @@ export class CrowdstrikeAgentStatusClient extends AgentStatusClient { const agentStatuses = await this.getAgentStatusFromConnectorAction(agentIds); return agentIds.reduce((acc, agentId) => { - const { device, crowdstrike } = mostRecentAgentInfosByAgentId[agentId]; + const { device, crowdstrike } = mostRecentAgentInfosByAgentId[agentId] || {}; const agentStatus = agentStatuses[agentId]; const pendingActions = allPendingActions.find( diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/constants.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/constants.ts index c221d74c3b8a0..31b54a7599a88 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/constants.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/constants.ts @@ -13,4 +13,5 @@ export enum SUB_ACTION { GET_AGENT_DETAILS = 'getAgentDetails', HOST_ACTIONS = 'hostActions', GET_AGENT_ONLINE_STATUS = 'getAgentOnlineStatus', + BATCH_INIT_RTR_SESSION = 'batchInitRTRSession', } diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts index 4e3d6464821b6..3c7c47ea88875 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts @@ -261,3 +261,45 @@ export const CrowdstrikeHostActionsSchema = schema.object({ }); export const CrowdstrikeActionParamsSchema = schema.oneOf([CrowdstrikeHostActionsSchema]); + +export const CrowdstrikeInitRTRResponseSchema = schema.object( + { + meta: schema.maybe( + schema.object( + { + query_time: schema.maybe(schema.number()), + powered_by: schema.maybe(schema.string()), + trace_id: schema.maybe(schema.string()), + }, + { unknowns: 'allow' } + ) + ), + batch_id: schema.maybe(schema.string()), + resources: schema.maybe( + schema.recordOf( + 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()), + }, + { unknowns: 'allow' } + ) + ) + ), + errors: schema.maybe(schema.arrayOf(schema.any())), + }, + { unknowns: 'allow' } +); + +export const CrowdstrikeInitRTRParamsSchema = schema.object({ + endpoint_ids: schema.arrayOf(schema.string()), +}); diff --git a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts index c0f98ee1b90a1..3c9cc15ea167e 100644 --- a/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts +++ b/x-pack/plugins/stack_connectors/common/crowdstrike/types.ts @@ -17,6 +17,8 @@ import { CrowdstrikeGetTokenResponseSchema, CrowdstrikeGetAgentsResponseSchema, RelaxedCrowdstrikeBaseApiResponseSchema, + CrowdstrikeInitRTRResponseSchema, + CrowdstrikeInitRTRParamsSchema, } from './schema'; export type CrowdstrikeConfig = TypeOf; @@ -33,7 +35,9 @@ 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; diff --git a/x-pack/plugins/stack_connectors/common/experimental_features.ts b/x-pack/plugins/stack_connectors/common/experimental_features.ts index 9c81371b4b458..ff4d2a41c30a6 100644 --- a/x-pack/plugins/stack_connectors/common/experimental_features.ts +++ b/x-pack/plugins/stack_connectors/common/experimental_features.ts @@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ sentinelOneConnectorOn: true, crowdstrikeConnectorOn: true, inferenceConnectorOn: false, + crowdstrikeConnectorRTROn: true, }); export type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.test.ts index 0c3d851981fde..eec431d8a4dcf 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.test.ts @@ -18,14 +18,18 @@ const onlineStatusPath = 'https://api.crowdstrike.com/devices/entities/online-st const actionsPath = 'https://api.crowdstrike.com/devices/entities/devices-actions/v2'; describe('CrowdstrikeConnector', () => { const logger = loggingSystemMock.createLogger(); - const connector = new CrowdstrikeConnector({ - configurationUtilities: actionsConfigMock.create(), - connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID }, - config: { url: 'https://api.crowdstrike.com' }, - secrets: { clientId: '123', clientSecret: 'secret' }, - logger, - services: actionsMock.createServices(), - }); + const connector = new CrowdstrikeConnector( + { + configurationUtilities: actionsConfigMock.create(), + connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID }, + config: { url: 'https://api.crowdstrike.com' }, + secrets: { clientId: '123', clientSecret: 'secret' }, + logger, + services: actionsMock.createServices(), + }, + // @ts-expect-error passing a true value just for testing purposes + { crowdstrikeConnectorRTROn: true } + ); let mockedRequest: jest.Mock; let connectorUsageCollector: ConnectorUsageCollector; @@ -341,4 +345,70 @@ describe('CrowdstrikeConnector', () => { expect(mockedRequest).toHaveBeenCalledTimes(3); }); }); + describe('batchInitRTRSession', () => { + it('should make a POST request to the correct URL with correct data', async () => { + const mockResponse = { data: { batch_id: 'testBatchId' } }; + mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } }); + mockedRequest.mockResolvedValueOnce(mockResponse); + + await connector.batchInitRTRSession( + { endpoint_ids: ['id1', 'id2'] }, + connectorUsageCollector + ); + + expect(mockedRequest).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + headers: { + accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + authorization: expect.any(String), + }, + method: 'post', + responseSchema: expect.any(Object), + url: tokenPath, + }), + connectorUsageCollector + ); + expect(mockedRequest).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + url: 'https://api.crowdstrike.com/real-time-response/combined/batch-init-session/v1', + method: 'post', + data: { host_ids: ['id1', 'id2'] }, + paramsSerializer: expect.any(Function), + responseSchema: expect.any(Object), + }), + connectorUsageCollector + ); + // @ts-expect-error private static - but I still want to test it + expect(CrowdstrikeConnector.currentBatchId).toBe('testBatchId'); + }); + + it('should handle error when fetching batch init session', async () => { + mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } }); + mockedRequest.mockRejectedValueOnce(new Error('Failed to fetch batch init session')); + + await expect( + connector.batchInitRTRSession({ endpoint_ids: ['id1', 'id2'] }, connectorUsageCollector) + ).rejects.toThrow('Failed to fetch batch init session'); + }); + + it('should retry once if token is invalid', async () => { + const mockResponse = { data: { batch_id: 'testBatchId' } }; + mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } }); + mockedRequest.mockRejectedValueOnce({ code: 401 }); + mockedRequest.mockResolvedValueOnce({ data: { access_token: 'newTestToken' } }); + mockedRequest.mockResolvedValueOnce(mockResponse); + + await connector.batchInitRTRSession( + { endpoint_ids: ['id1', 'id2'] }, + connectorUsageCollector + ); + + expect(mockedRequest).toHaveBeenCalledTimes(4); + // @ts-expect-error private static - but I still want to test it + expect(CrowdstrikeConnector.currentBatchId).toBe('testBatchId'); + }); + }); }); 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 a4fc84ae6a49a..0e15ed08e3888 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 @@ -10,6 +10,7 @@ import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server'; import type { AxiosError } from 'axios'; import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; import { isAggregateError, NodeSystemError } from './types'; import type { CrowdstrikeConfig, @@ -20,6 +21,7 @@ import type { CrowdstrikeGetTokenResponse, CrowdstrikeGetAgentOnlineStatusResponse, RelaxedCrowdstrikeBaseApiResponse, + CrowdstrikeInitRTRParams, } from '../../../common/crowdstrike/types'; import { CrowdstrikeHostActionsParamsSchema, @@ -27,6 +29,8 @@ import { CrowdstrikeGetTokenResponseSchema, CrowdstrikeHostActionsResponseSchema, RelaxedCrowdstrikeBaseApiResponseSchema, + CrowdstrikeInitRTRParamsSchema, + CrowdstrikeInitRTRResponseSchema, } from '../../../common/crowdstrike/schema'; import { SUB_ACTION } from '../../../common/crowdstrike/constants'; import { CrowdstrikeError } from './error'; @@ -51,21 +55,31 @@ export class CrowdstrikeConnector extends SubActionConnector< > { private static token: string | null; private static tokenExpiryTimeout: NodeJS.Timeout; + // @ts-expect-error not used at the moment, will be used in a follow up PR + private static currentBatchId: string | undefined; private static base64encodedToken: string; + private experimentalFeatures: ExperimentalFeatures; + private urls: { getToken: string; agents: string; hostAction: string; agentStatus: string; + batchInitRTRSession: string; }; - constructor(params: ServiceParams) { + constructor( + params: ServiceParams, + experimentalFeatures: ExperimentalFeatures + ) { super(params); + this.experimentalFeatures = experimentalFeatures; this.urls = { getToken: `${this.config.url}/oauth2/token`, hostAction: `${this.config.url}/devices/entities/devices-actions/v2`, agents: `${this.config.url}/devices/entities/devices/v2`, agentStatus: `${this.config.url}/devices/entities/online-state/v1`, + batchInitRTRSession: `${this.config.url}/real-time-response/combined/batch-init-session/v1`, }; if (!CrowdstrikeConnector.base64encodedToken) { @@ -95,6 +109,13 @@ export class CrowdstrikeConnector extends SubActionConnector< method: 'getAgentOnlineStatus', schema: CrowdstrikeGetAgentsParamsSchema, }); + if (this.experimentalFeatures.crowdstrikeConnectorRTROn) { + this.registerSubAction({ + name: SUB_ACTION.BATCH_INIT_RTR_SESSION, + method: 'batchInitRTRSession', + schema: CrowdstrikeInitRTRParamsSchema, + }); + } } public async executeHostActions( @@ -224,6 +245,26 @@ export class CrowdstrikeConnector extends SubActionConnector< } } + public async batchInitRTRSession( + payload: CrowdstrikeInitRTRParams, + connectorUsageCollector: ConnectorUsageCollector + ) { + const response = await this.crowdstrikeApiRequest( + { + url: this.urls.batchInitRTRSession, + method: 'post', + data: { + host_ids: payload.endpoint_ids, + }, + paramsSerializer, + responseSchema: CrowdstrikeInitRTRResponseSchema, + }, + connectorUsageCollector + ); + + CrowdstrikeConnector.currentBatchId = response.batch_id; + } + protected getResponseErrorMessage( error: AxiosError<{ errors: Array<{ message: string; code: number }> }> ): string { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/index.ts index f7c50478979c7..0617822837c0a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/index.ts @@ -11,6 +11,7 @@ import { } from '@kbn/actions-plugin/server/sub_action_framework/types'; import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { urlAllowListValidator } from '@kbn/actions-plugin/server'; +import { ExperimentalFeatures } from '../../../common/experimental_features'; import { CROWDSTRIKE_CONNECTOR_ID, CROWDSTRIKE_TITLE } from '../../../common/crowdstrike/constants'; import { CrowdstrikeConfigSchema, @@ -19,13 +20,12 @@ import { import { CrowdstrikeConfig, CrowdstrikeSecrets } from '../../../common/crowdstrike/types'; import { CrowdstrikeConnector } from './crowdstrike'; -export const getCrowdstrikeConnectorType = (): SubActionConnectorType< - CrowdstrikeConfig, - CrowdstrikeSecrets -> => ({ +export const getCrowdstrikeConnectorType = ( + experimentalFeatures: ExperimentalFeatures +): SubActionConnectorType => ({ id: CROWDSTRIKE_CONNECTOR_ID, name: CROWDSTRIKE_TITLE, - getService: (params) => new CrowdstrikeConnector(params), + getService: (params) => new CrowdstrikeConnector(params, experimentalFeatures), schema: { config: CrowdstrikeConfigSchema, secrets: CrowdstrikeSecretsSchema, diff --git a/x-pack/plugins/stack_connectors/server/connector_types/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/index.ts index 09d8a44c2a287..a156547cc2fa6 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/index.ts @@ -117,7 +117,7 @@ export function registerConnectorTypes({ actions.registerSubActionConnectorType(getSentinelOneConnectorType()); } if (experimentalFeatures.crowdstrikeConnectorOn) { - actions.registerSubActionConnectorType(getCrowdstrikeConnectorType()); + actions.registerSubActionConnectorType(getCrowdstrikeConnectorType(experimentalFeatures)); } if (experimentalFeatures.inferenceConnectorOn) { actions.registerSubActionConnectorType(getInferenceConnectorType());