Skip to content

Commit

Permalink
init session API
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsonpl committed Nov 22, 2024
1 parent ed1a35a commit 5abb502
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class CrowdstrikeAgentStatusClient extends AgentStatusClient {
const agentStatuses = await this.getAgentStatusFromConnectorAction(agentIds);

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

const agentStatus = agentStatuses[agentId];
const pendingActions = allPendingActions.find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
42 changes: 42 additions & 0 deletions x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
});
4 changes: 4 additions & 0 deletions x-pack/plugins/stack_connectors/common/crowdstrike/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
CrowdstrikeGetTokenResponseSchema,
CrowdstrikeGetAgentsResponseSchema,
RelaxedCrowdstrikeBaseApiResponseSchema,
CrowdstrikeInitRTRResponseSchema,
CrowdstrikeInitRTRParamsSchema,
} from './schema';

export type CrowdstrikeConfig = TypeOf<typeof CrowdstrikeConfigSchema>;
Expand All @@ -33,7 +35,9 @@ export type CrowdstrikeGetAgentOnlineStatusResponse = TypeOf<
typeof CrowdstrikeGetAgentOnlineStatusResponseSchema
>;
export type CrowdstrikeGetTokenResponse = TypeOf<typeof CrowdstrikeGetTokenResponseSchema>;
export type CrowdstrikeInitRTRResponse = TypeOf<typeof CrowdstrikeInitRTRResponseSchema>;

export type CrowdstrikeHostActionsParams = TypeOf<typeof CrowdstrikeHostActionsParamsSchema>;

export type CrowdstrikeActionParams = TypeOf<typeof CrowdstrikeActionParamsSchema>;
export type CrowdstrikeInitRTRParams = TypeOf<typeof CrowdstrikeInitRTRParamsSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
sentinelOneConnectorOn: true,
crowdstrikeConnectorOn: true,
inferenceConnectorOn: false,
crowdstrikeConnectorRTROn: true,
});

export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,13 +21,16 @@ import type {
CrowdstrikeGetTokenResponse,
CrowdstrikeGetAgentOnlineStatusResponse,
RelaxedCrowdstrikeBaseApiResponse,
CrowdstrikeInitRTRParams,
} from '../../../common/crowdstrike/types';
import {
CrowdstrikeHostActionsParamsSchema,
CrowdstrikeGetAgentsParamsSchema,
CrowdstrikeGetTokenResponseSchema,
CrowdstrikeHostActionsResponseSchema,
RelaxedCrowdstrikeBaseApiResponseSchema,
CrowdstrikeInitRTRParamsSchema,
CrowdstrikeInitRTRResponseSchema,
} from '../../../common/crowdstrike/schema';
import { SUB_ACTION } from '../../../common/crowdstrike/constants';
import { CrowdstrikeError } from './error';
Expand All @@ -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<CrowdstrikeConfig, CrowdstrikeSecrets>) {
constructor(
params: ServiceParams<CrowdstrikeConfig, CrowdstrikeSecrets>,
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) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<CrowdstrikeConfig, CrowdstrikeSecrets> => ({
id: CROWDSTRIKE_CONNECTOR_ID,
name: CROWDSTRIKE_TITLE,
getService: (params) => new CrowdstrikeConnector(params),
getService: (params) => new CrowdstrikeConnector(params, experimentalFeatures),
schema: {
config: CrowdstrikeConfigSchema,
secrets: CrowdstrikeSecretsSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down

0 comments on commit 5abb502

Please sign in to comment.