Skip to content

Commit 99a4135

Browse files
authored
[EDR Workflows] Initialize CrowdStrike session API (#201420)
1 parent 2890570 commit 99a4135

File tree

11 files changed

+492
-16
lines changed

11 files changed

+492
-16
lines changed

x-pack/plugins/security_solution/server/endpoint/services/agent/clients/crowdstrike/crowdstrike_agent_status_client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class CrowdstrikeAgentStatusClient extends AgentStatusClient {
132132
const agentStatuses = await this.getAgentStatusFromConnectorAction(agentIds);
133133

134134
return agentIds.reduce<AgentStatusRecords>((acc, agentId) => {
135-
const { device, crowdstrike } = mostRecentAgentInfosByAgentId[agentId];
135+
const { device, crowdstrike } = mostRecentAgentInfosByAgentId[agentId] || {};
136136

137137
const agentStatus = agentStatuses[agentId];
138138
const pendingActions = allPendingActions.find(

x-pack/plugins/stack_connectors/common/crowdstrike/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export enum SUB_ACTION {
1313
GET_AGENT_DETAILS = 'getAgentDetails',
1414
HOST_ACTIONS = 'hostActions',
1515
GET_AGENT_ONLINE_STATUS = 'getAgentOnlineStatus',
16+
EXECUTE_RTR_COMMAND = 'executeRTRCommand',
1617
}

x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ export const CrowdstrikeHostActionsResponseSchema = schema.object(
231231
{ unknowns: 'allow' }
232232
);
233233

234+
// TODO temporary any value
235+
export const CrowdstrikeRTRCommandParamsSchema = schema.any();
234236
export const CrowdstrikeHostActionsParamsSchema = schema.object({
235237
command: schema.oneOf([schema.literal('contain'), schema.literal('lift_containment')]),
236238
actionParameters: schema.maybe(schema.object({}, { unknowns: 'allow' })),
@@ -261,3 +263,45 @@ export const CrowdstrikeHostActionsSchema = schema.object({
261263
});
262264

263265
export const CrowdstrikeActionParamsSchema = schema.oneOf([CrowdstrikeHostActionsSchema]);
266+
267+
export const CrowdstrikeInitRTRResponseSchema = schema.object(
268+
{
269+
meta: schema.maybe(
270+
schema.object(
271+
{
272+
query_time: schema.maybe(schema.number()),
273+
powered_by: schema.maybe(schema.string()),
274+
trace_id: schema.maybe(schema.string()),
275+
},
276+
{ unknowns: 'allow' }
277+
)
278+
),
279+
batch_id: schema.maybe(schema.string()),
280+
resources: schema.maybe(
281+
schema.recordOf(
282+
schema.string(),
283+
schema.object(
284+
{
285+
session_id: schema.maybe(schema.string()),
286+
task_id: schema.maybe(schema.string()),
287+
complete: schema.maybe(schema.boolean()),
288+
stdout: schema.maybe(schema.string()),
289+
stderr: schema.maybe(schema.string()),
290+
base_command: schema.maybe(schema.string()),
291+
aid: schema.maybe(schema.string()),
292+
errors: schema.maybe(schema.arrayOf(schema.any())),
293+
query_time: schema.maybe(schema.number()),
294+
offline_queued: schema.maybe(schema.boolean()),
295+
},
296+
{ unknowns: 'allow' }
297+
)
298+
)
299+
),
300+
errors: schema.maybe(schema.arrayOf(schema.any())),
301+
},
302+
{ unknowns: 'allow' }
303+
);
304+
305+
export const CrowdstrikeInitRTRParamsSchema = schema.object({
306+
endpoint_ids: schema.arrayOf(schema.string()),
307+
});

x-pack/plugins/stack_connectors/common/crowdstrike/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
CrowdstrikeGetTokenResponseSchema,
1818
CrowdstrikeGetAgentsResponseSchema,
1919
RelaxedCrowdstrikeBaseApiResponseSchema,
20+
CrowdstrikeInitRTRResponseSchema,
21+
CrowdstrikeInitRTRParamsSchema,
2022
} from './schema';
2123

2224
export type CrowdstrikeConfig = TypeOf<typeof CrowdstrikeConfigSchema>;
@@ -33,7 +35,9 @@ export type CrowdstrikeGetAgentOnlineStatusResponse = TypeOf<
3335
typeof CrowdstrikeGetAgentOnlineStatusResponseSchema
3436
>;
3537
export type CrowdstrikeGetTokenResponse = TypeOf<typeof CrowdstrikeGetTokenResponseSchema>;
38+
export type CrowdstrikeInitRTRResponse = TypeOf<typeof CrowdstrikeInitRTRResponseSchema>;
3639

3740
export type CrowdstrikeHostActionsParams = TypeOf<typeof CrowdstrikeHostActionsParamsSchema>;
3841

3942
export type CrowdstrikeActionParams = TypeOf<typeof CrowdstrikeActionParamsSchema>;
43+
export type CrowdstrikeInitRTRParams = TypeOf<typeof CrowdstrikeInitRTRParamsSchema>;

x-pack/plugins/stack_connectors/common/experimental_features.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
1616
sentinelOneConnectorOn: true,
1717
crowdstrikeConnectorOn: true,
1818
inferenceConnectorOn: false,
19+
crowdstrikeConnectorRTROn: false,
1920
});
2021

2122
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.test.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ const onlineStatusPath = 'https://api.crowdstrike.com/devices/entities/online-st
1818
const actionsPath = 'https://api.crowdstrike.com/devices/entities/devices-actions/v2';
1919
describe('CrowdstrikeConnector', () => {
2020
const logger = loggingSystemMock.createLogger();
21-
const connector = new CrowdstrikeConnector({
22-
configurationUtilities: actionsConfigMock.create(),
23-
connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID },
24-
config: { url: 'https://api.crowdstrike.com' },
25-
secrets: { clientId: '123', clientSecret: 'secret' },
26-
logger,
27-
services: actionsMock.createServices(),
28-
});
21+
const connector = new CrowdstrikeConnector(
22+
{
23+
configurationUtilities: actionsConfigMock.create(),
24+
connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID },
25+
config: { url: 'https://api.crowdstrike.com' },
26+
secrets: { clientId: '123', clientSecret: 'secret' },
27+
logger,
28+
services: actionsMock.createServices(),
29+
},
30+
// @ts-expect-error passing a true value just for testing purposes
31+
{ crowdstrikeConnectorRTROn: true }
32+
);
2933
let mockedRequest: jest.Mock;
3034
let connectorUsageCollector: ConnectorUsageCollector;
3135

@@ -341,4 +345,70 @@ describe('CrowdstrikeConnector', () => {
341345
expect(mockedRequest).toHaveBeenCalledTimes(3);
342346
});
343347
});
348+
describe('batchInitRTRSession', () => {
349+
it('should make a POST request to the correct URL with correct data', async () => {
350+
const mockResponse = { data: { batch_id: 'testBatchId' } };
351+
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
352+
mockedRequest.mockResolvedValueOnce(mockResponse);
353+
354+
await connector.batchInitRTRSession(
355+
{ endpoint_ids: ['id1', 'id2'] },
356+
connectorUsageCollector
357+
);
358+
359+
expect(mockedRequest).toHaveBeenNthCalledWith(
360+
1,
361+
expect.objectContaining({
362+
headers: {
363+
accept: 'application/json',
364+
'Content-Type': 'application/x-www-form-urlencoded',
365+
authorization: expect.any(String),
366+
},
367+
method: 'post',
368+
responseSchema: expect.any(Object),
369+
url: tokenPath,
370+
}),
371+
connectorUsageCollector
372+
);
373+
expect(mockedRequest).toHaveBeenNthCalledWith(
374+
2,
375+
expect.objectContaining({
376+
url: 'https://api.crowdstrike.com/real-time-response/combined/batch-init-session/v1',
377+
method: 'post',
378+
data: { host_ids: ['id1', 'id2'] },
379+
paramsSerializer: expect.any(Function),
380+
responseSchema: expect.any(Object),
381+
}),
382+
connectorUsageCollector
383+
);
384+
// @ts-expect-error private static - but I still want to test it
385+
expect(CrowdstrikeConnector.currentBatchId).toBe('testBatchId');
386+
});
387+
388+
it('should handle error when fetching batch init session', async () => {
389+
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
390+
mockedRequest.mockRejectedValueOnce(new Error('Failed to fetch batch init session'));
391+
392+
await expect(
393+
connector.batchInitRTRSession({ endpoint_ids: ['id1', 'id2'] }, connectorUsageCollector)
394+
).rejects.toThrow('Failed to fetch batch init session');
395+
});
396+
397+
it('should retry once if token is invalid', async () => {
398+
const mockResponse = { data: { batch_id: 'testBatchId' } };
399+
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
400+
mockedRequest.mockRejectedValueOnce({ code: 401 });
401+
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'newTestToken' } });
402+
mockedRequest.mockResolvedValueOnce(mockResponse);
403+
404+
await connector.batchInitRTRSession(
405+
{ endpoint_ids: ['id1', 'id2'] },
406+
connectorUsageCollector
407+
);
408+
409+
expect(mockedRequest).toHaveBeenCalledTimes(4);
410+
// @ts-expect-error private static - but I still want to test it
411+
expect(CrowdstrikeConnector.currentBatchId).toBe('testBatchId');
412+
});
413+
});
344414
});

x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/crowdstrike.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
1010
import type { AxiosError } from 'axios';
1111
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
1212
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
13+
import { CrowdStrikeSessionManager } from './rtr_session_manager';
14+
import { ExperimentalFeatures } from '../../../common/experimental_features';
1315
import { isAggregateError, NodeSystemError } from './types';
1416
import type {
1517
CrowdstrikeConfig,
@@ -20,13 +22,16 @@ import type {
2022
CrowdstrikeGetTokenResponse,
2123
CrowdstrikeGetAgentOnlineStatusResponse,
2224
RelaxedCrowdstrikeBaseApiResponse,
25+
CrowdstrikeInitRTRParams,
2326
} from '../../../common/crowdstrike/types';
2427
import {
2528
CrowdstrikeHostActionsParamsSchema,
2629
CrowdstrikeGetAgentsParamsSchema,
2730
CrowdstrikeGetTokenResponseSchema,
2831
CrowdstrikeHostActionsResponseSchema,
2932
RelaxedCrowdstrikeBaseApiResponseSchema,
33+
CrowdstrikeInitRTRResponseSchema,
34+
CrowdstrikeRTRCommandParamsSchema,
3035
} from '../../../common/crowdstrike/schema';
3136
import { SUB_ACTION } from '../../../common/crowdstrike/constants';
3237
import { CrowdstrikeError } from './error';
@@ -51,21 +56,34 @@ export class CrowdstrikeConnector extends SubActionConnector<
5156
> {
5257
private static token: string | null;
5358
private static tokenExpiryTimeout: NodeJS.Timeout;
59+
// @ts-expect-error not used at the moment, will be used in a follow up PR
60+
private static currentBatchId: string | undefined;
5461
private static base64encodedToken: string;
62+
private experimentalFeatures: ExperimentalFeatures;
63+
64+
private crowdStrikeSessionManager: CrowdStrikeSessionManager;
5565
private urls: {
5666
getToken: string;
5767
agents: string;
5868
hostAction: string;
5969
agentStatus: string;
70+
batchInitRTRSession: string;
71+
batchRefreshRTRSession: string;
6072
};
6173

62-
constructor(params: ServiceParams<CrowdstrikeConfig, CrowdstrikeSecrets>) {
74+
constructor(
75+
params: ServiceParams<CrowdstrikeConfig, CrowdstrikeSecrets>,
76+
experimentalFeatures: ExperimentalFeatures
77+
) {
6378
super(params);
79+
this.experimentalFeatures = experimentalFeatures;
6480
this.urls = {
6581
getToken: `${this.config.url}/oauth2/token`,
6682
hostAction: `${this.config.url}/devices/entities/devices-actions/v2`,
6783
agents: `${this.config.url}/devices/entities/devices/v2`,
6884
agentStatus: `${this.config.url}/devices/entities/online-state/v1`,
85+
batchInitRTRSession: `${this.config.url}/real-time-response/combined/batch-init-session/v1`,
86+
batchRefreshRTRSession: `${this.config.url}/real-time-response/combined/batch-refresh-session/v1`,
6987
};
7088

7189
if (!CrowdstrikeConnector.base64encodedToken) {
@@ -74,6 +92,10 @@ export class CrowdstrikeConnector extends SubActionConnector<
7492
).toString('base64');
7593
}
7694

95+
this.crowdStrikeSessionManager = new CrowdStrikeSessionManager(
96+
this.urls,
97+
this.crowdstrikeApiRequest
98+
);
7799
this.registerSubActions();
78100
}
79101

@@ -95,6 +117,14 @@ export class CrowdstrikeConnector extends SubActionConnector<
95117
method: 'getAgentOnlineStatus',
96118
schema: CrowdstrikeGetAgentsParamsSchema,
97119
});
120+
121+
if (this.experimentalFeatures.crowdstrikeConnectorRTROn) {
122+
this.registerSubAction({
123+
name: SUB_ACTION.EXECUTE_RTR_COMMAND,
124+
method: 'executeRTRCommand',
125+
schema: CrowdstrikeRTRCommandParamsSchema, // Define a proper schema for the command
126+
});
127+
}
98128
}
99129

100130
public async executeHostActions(
@@ -224,6 +254,39 @@ export class CrowdstrikeConnector extends SubActionConnector<
224254
}
225255
}
226256

257+
public async batchInitRTRSession(
258+
payload: CrowdstrikeInitRTRParams,
259+
connectorUsageCollector: ConnectorUsageCollector
260+
) {
261+
const response = await this.crowdstrikeApiRequest(
262+
{
263+
url: this.urls.batchInitRTRSession,
264+
method: 'post',
265+
data: {
266+
host_ids: payload.endpoint_ids,
267+
},
268+
paramsSerializer,
269+
responseSchema: CrowdstrikeInitRTRResponseSchema,
270+
},
271+
connectorUsageCollector
272+
);
273+
274+
CrowdstrikeConnector.currentBatchId = response.batch_id;
275+
}
276+
277+
// TODO: WIP - just to have session init logic in place
278+
public async executeRTRCommand(
279+
payload: { command: string; endpoint_ids: string[] },
280+
connectorUsageCollector: ConnectorUsageCollector
281+
) {
282+
const batchId = await this.crowdStrikeSessionManager.initializeSession(
283+
{ endpoint_ids: payload.endpoint_ids },
284+
connectorUsageCollector
285+
);
286+
287+
return Promise.resolve({ batchId });
288+
}
289+
227290
protected getResponseErrorMessage(
228291
error: AxiosError<{ errors: Array<{ message: string; code: number }> }>
229292
): string {

x-pack/plugins/stack_connectors/server/connector_types/crowdstrike/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@kbn/actions-plugin/server/sub_action_framework/types';
1212
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
1313
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
14+
import { ExperimentalFeatures } from '../../../common/experimental_features';
1415
import { CROWDSTRIKE_CONNECTOR_ID, CROWDSTRIKE_TITLE } from '../../../common/crowdstrike/constants';
1516
import {
1617
CrowdstrikeConfigSchema,
@@ -19,13 +20,12 @@ import {
1920
import { CrowdstrikeConfig, CrowdstrikeSecrets } from '../../../common/crowdstrike/types';
2021
import { CrowdstrikeConnector } from './crowdstrike';
2122

22-
export const getCrowdstrikeConnectorType = (): SubActionConnectorType<
23-
CrowdstrikeConfig,
24-
CrowdstrikeSecrets
25-
> => ({
23+
export const getCrowdstrikeConnectorType = (
24+
experimentalFeatures: ExperimentalFeatures
25+
): SubActionConnectorType<CrowdstrikeConfig, CrowdstrikeSecrets> => ({
2626
id: CROWDSTRIKE_CONNECTOR_ID,
2727
name: CROWDSTRIKE_TITLE,
28-
getService: (params) => new CrowdstrikeConnector(params),
28+
getService: (params) => new CrowdstrikeConnector(params, experimentalFeatures),
2929
schema: {
3030
config: CrowdstrikeConfigSchema,
3131
secrets: CrowdstrikeSecretsSchema,

0 commit comments

Comments
 (0)