Skip to content

Commit

Permalink
[EDR Workflows] CrowdStrike RTR connector's sub actions (elastic#203420)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsonpl authored Dec 11, 2024
1 parent 751c86b commit 5be7182
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ export enum SUB_ACTION {
HOST_ACTIONS = 'hostActions',
GET_AGENT_ONLINE_STATUS = 'getAgentOnlineStatus',
EXECUTE_RTR_COMMAND = 'executeRTRCommand',
EXECUTE_ACTIVE_RESPONDER_RTR = 'batchActiveResponderExecuteRTR',
EXECUTE_ADMIN_RTR = 'batchAdminExecuteRTR',
GET_RTR_CLOUD_SCRIPTS = 'getRTRCloudScripts',
}
43 changes: 43 additions & 0 deletions x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,46 @@ export const CrowdstrikeInitRTRResponseSchema = schema.object(
export const CrowdstrikeInitRTRParamsSchema = schema.object({
endpoint_ids: schema.arrayOf(schema.string()),
});

export const CrowdstrikeExecuteRTRResponseSchema = schema.object(
{
combined: schema.object(
{
resources: 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' }
)
),
},
{ unknowns: 'allow' }
),
meta: schema.object(
{
query_time: schema.maybe(schema.number()),
powered_by: schema.maybe(schema.string()),
trace_id: schema.maybe(schema.string()),
},
{ unknowns: 'allow' }
),
errors: schema.nullable(schema.arrayOf(schema.any())),
},
{ unknowns: 'allow' }
);

export type CrowdStrikeExecuteRTRResponse = typeof CrowdstrikeExecuteRTRResponseSchema;

// TODO: will be part of a next PR
export const CrowdstrikeGetScriptsParamsSchema = schema.any({});
Original file line number Diff line number Diff line change
Expand Up @@ -345,70 +345,185 @@ describe('CrowdstrikeConnector', () => {
expect(mockedRequest).toHaveBeenCalledTimes(3);
});
});
describe('batchInitRTRSession', () => {
describe('executeRTRCommand', () => {
it('should make a POST request to the correct URL with correct data', async () => {
const mockResponse = { data: { batch_id: 'testBatchId' } };
const mockResponse = { data: obfuscatedRTRResponse };

mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
mockedRequest.mockResolvedValueOnce(mockResponse);
mockedRequest.mockResolvedValue(mockResponse);

await connector.batchInitRTRSession(
{ endpoint_ids: ['id1', 'id2'] },
const result = await connector.executeRTRCommand(
{
command: 'runscript -Raw',
endpoint_ids: ['id1', 'id2'],
},
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
url: 'https://api.crowdstrike.com/real-time-response/combined/batch-command/v1',
method: 'post',
data: expect.objectContaining({
command_string: 'runscript -Raw',
hosts: ['id1', 'id2'],
}),
}),
connectorUsageCollector
);

expect(result).toEqual(obfuscatedRTRResponse);
});
});

describe('batchActiveResponderExecuteRTR', () => {
it('should make a POST request to the correct URL with correct data', async () => {
const mockResponse = { data: obfuscatedRTRResponse };

mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
mockedRequest.mockResolvedValue(mockResponse);

const result = await connector.batchActiveResponderExecuteRTR(
{
command: 'runscript',
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),
},
url: 'https://api.crowdstrike.com/oauth2/token',
method: 'post',
responseSchema: expect.any(Object),
url: tokenPath,
}),
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenNthCalledWith(
2,
3,
expect.objectContaining({
url: 'https://api.crowdstrike.com/real-time-response/combined/batch-init-session/v1',
url: 'https://api.crowdstrike.com/real-time-response/combined/batch-active-responder-command/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');

expect(result).toEqual(obfuscatedRTRResponse);
});
});

describe('batchAdminExecuteRTR', () => {
it('should make a POST request to the correct URL with correct data', async () => {
const mockResponse = { data: obfuscatedRTRResponse };

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'));
mockedRequest.mockResolvedValue(mockResponse);

const result = await connector.batchAdminExecuteRTR(
{
command: 'runscript',
endpoint_ids: ['id1', 'id2'],
},
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
url: 'https://api.crowdstrike.com/oauth2/token',
method: 'post',
}),
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
url: 'https://api.crowdstrike.com/real-time-response/combined/batch-admin-command/v1',
method: 'post',
}),
connectorUsageCollector
);

await expect(
connector.batchInitRTRSession({ endpoint_ids: ['id1', 'id2'] }, connectorUsageCollector)
).rejects.toThrow('Failed to fetch batch init session');
expect(result).toEqual(obfuscatedRTRResponse);
});
});

describe('getRTRCloudScripts', () => {
it('should make a GET request to the correct URL with correct params', async () => {
const mockResponse = { data: { scripts: [{}] } };

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'] },
const result = await connector.getRTRCloudScripts(
{ ids: ['script1', 'script2'] },
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
url: 'https://api.crowdstrike.com/oauth2/token',
method: 'post',
}),
connectorUsageCollector
);

expect(mockedRequest).toHaveBeenCalledTimes(4);
// @ts-expect-error private static - but I still want to test it
expect(CrowdstrikeConnector.currentBatchId).toBe('testBatchId');
expect(mockedRequest).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
url: 'https://api.crowdstrike.com/real-time-response/entities/scripts/v1',
method: 'GET',
}),
connectorUsageCollector
);

expect(result).toEqual({ scripts: [{}] });
});
});
});

const obfuscatedRTRResponse = {
combined: {
resources: {
host1: {
session_id: 'abcdef123456',
task_id: 'task123',
complete: true,
stdout:
'bin \n boot \n dev \n etc \n home \n lib \n lib64 \n media \n mnt \n opt \n proc \n root \n run \n sbin \n srv \n sys \n tmp \n usr \n var \n',
stderr: '',
base_command: 'runscript',
aid: 'aid123',
errors: [{ message: 'Error example', code: 123 }],
query_time: 1234567890,
offline_queued: false,
},
host2: {
session_id: 'ghijkl789101',
task_id: 'task456',
complete: false,
stdout:
'bin \n boot \n dev \n etc \n home \n lib \n lib64 \n media \n mnt \n opt \n proc \n root \n run \n sbin \n srv \n sys \n tmp \n usr \n var \n',
stderr: '',
base_command: 'getscripts',
aid: 'aid456',
errors: null,
query_time: 9876543210,
offline_queued: true,
},
},
},
meta: {
query_time: 1234567890,
powered_by: 'CrowdStrike',
trace_id: 'trace-abcdef123456',
},
errors: [
{ message: 'An example error', code: 500 },
{ message: 'Another error example', code: 404 },
],
};
Loading

0 comments on commit 5be7182

Please sign in to comment.