[Security Solution][Endpoint] SentinelOne API support for get-file response action (#180856)

## Summary

With this PR:

- Adds API support for `get-file` for SentinelOne agent types
- New feature flag to gate this functionality -
`responseActionsSentinelOneGetFileEnabled`
This commit is contained in:
Paul Tavares 2024-04-22 14:33:53 -04:00 committed by GitHub
parent 9ea94d10b6
commit 6b0e38ad4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 479 additions and 62 deletions

View file

@ -92,7 +92,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
unisolate: {
automated: {
endpoint: true,
endpoint: false,
sentinel_one: false,
},
manual: {
@ -102,7 +102,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
upload: {
automated: {
endpoint: true,
endpoint: false,
sentinel_one: false,
},
manual: {
@ -112,12 +112,12 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
'get-file': {
automated: {
endpoint: true,
endpoint: false,
sentinel_one: false,
},
manual: {
endpoint: true,
sentinel_one: false,
sentinel_one: true,
},
},
'kill-process': {
@ -132,7 +132,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
execute: {
automated: {
endpoint: true,
endpoint: false,
sentinel_one: false,
},
manual: {
@ -152,7 +152,7 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = {
},
'running-processes': {
automated: {
endpoint: true,
endpoint: false,
sentinel_one: false,
},
manual: {

View file

@ -9,3 +9,9 @@
* Index name where the SentinelOne activity log is written to by the SentinelOne integration
*/
export const SENTINEL_ONE_ACTIVITY_INDEX = 'logs-sentinel_one.activity-default';
/**
* The passcode to be used when initiating actions in SentinelOne that require a passcode to be
* set for the resulting zip file
*/
export const SENTINEL_ONE_ZIP_PASSCODE = 'Elastic@123';

View file

@ -53,3 +53,13 @@ export interface SentinelOneIsolationResponseMeta {
/** The SentinelOne activity log primary description */
activityLogEntryDescription: string;
}
export interface SentinelOneGetFileRequestMeta extends SentinelOneActionRequestCommonMeta {
/** The SentinelOne activity log entry id for the Get File request */
activityId: string;
/**
* The command batch UUID is a value that appears in both the Request and the Response, thus it
* is stored in the request to facilitate locating the response later by the background task
*/
commandBatchUuid: string;
}

View file

@ -85,6 +85,9 @@ export const allowedExperimentalValues = Object.freeze({
*/
responseActionsSentinelOneV2Enabled: false,
/** Enables the `get-file` response action for SentinelOne */
responseActionsSentinelOneGetFileEnabled: false,
/**
* 8.15
* Enables use of agent status service to get agent status information

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import type { SentinelOneGetAgentsResponse } from '@kbn/stack-connectors-plugin/common/sentinelone/types';
import type {
SentinelOneGetAgentsResponse,
SentinelOneGetActivitiesResponse,
} from '@kbn/stack-connectors-plugin/common/sentinelone/types';
import {
SENTINELONE_CONNECTOR_ID,
SUB_ACTION,
@ -127,6 +130,58 @@ const createSentinelOneAgentDetailsMock = (
);
};
const createSentinelOneGetActivitiesApiResponseMock = (): SentinelOneGetActivitiesResponse => {
return {
errors: undefined,
pagination: {
nextCursor: null,
totalItems: 1,
},
data: [
{
accountId: '1392053568574369781',
accountName: 'Elastic',
activityType: 81,
activityUuid: 'ee9227f5-8f59-4f6d-bd46-3b74f93fd939',
agentId: '1913920934584665209',
agentUpdatedVersion: null,
comments: null,
createdAt: '2024-04-16T19:21:08.492444Z',
data: {
accountName: 'Elastic',
commandBatchUuid: '7011777f-77e7-4a01-a674-e5f767808895',
computerName: 'ptavares-sentinelone-1371',
externalIp: '108.77.84.191',
fullScopeDetails: 'Group Default Group in Site Default site of Account Elastic',
fullScopeDetailsPath: 'Global / Elastic / Default site / Default Group',
groupName: 'Default Group',
groupType: 'Manual',
ipAddress: '108.77.84.191',
scopeLevel: 'Group',
scopeName: 'Default Group',
siteName: 'Default site',
username: 'Defend Workflows Automation',
uuid: 'c06d63d9-9fa2-046d-e91e-dc94cf6695d8',
},
description: null,
groupId: '1392053568591146999',
groupName: 'Default Group',
hash: null,
id: '1929937418124016884',
osFamily: null,
primaryDescription:
'The management user Defend Workflows Automation initiated a fetch file command to the agent ptavares-sentinelone-1371 (108.77.84.191).',
secondaryDescription: 'IP address: 108.77.84.191',
siteId: '1392053568582758390',
siteName: 'Default site',
threatId: null,
updatedAt: '2024-04-16T19:21:08.492450Z',
userId: '1796254913836217560',
},
],
};
};
const createSentinelOneGetAgentsApiResponseMock = (
data: SentinelOneGetAgentsResponse['data'] = [createSentinelOneAgentDetailsMock()]
): SentinelOneGetAgentsResponse => {
@ -165,6 +220,11 @@ const createConnectorActionsClientMock = (): ActionsClientMock => {
data: createSentinelOneGetAgentsApiResponseMock(),
});
case SUB_ACTION.GET_ACTIVITIES:
return responseActionsClientMock.createConnectorActionExecuteResponse({
data: createSentinelOneGetActivitiesApiResponseMock(),
});
default:
return responseActionsClientMock.createConnectorActionExecuteResponse();
}
@ -188,4 +248,5 @@ export const sentinelOneMock = {
createSentinelOneAgentDetails: createSentinelOneAgentDetailsMock,
createConnectorActionsClient: createConnectorActionsClientMock,
createConstructorOptions: createConstructorOptionsMock,
createSentinelOneActivitiesApiResponse: createSentinelOneGetActivitiesApiResponseMock,
};

View file

@ -29,6 +29,9 @@ import type {
SentinelOneIsolationRequestMeta,
} from '../../../../../../common/endpoint/types';
import type { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import type { ResponseActionGetFileRequestBody } from '../../../../../../common/api/endpoint';
import { SENTINEL_ONE_ZIP_PASSCODE } from '../../../../../../common/endpoint/service/response_actions/sentinel_one';
import { SUB_ACTION } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
jest.mock('../../action_details_by_id', () => {
const originalMod = jest.requireActual('../../action_details_by_id');
@ -59,22 +62,14 @@ describe('SentinelOneActionsClient class', () => {
s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions);
});
it.each([
'killProcess',
'suspendProcess',
'runningProcesses',
'getFile',
'execute',
'upload',
] as Array<keyof ResponseActionsClient>)(
'should throw an un-supported error for %s',
async (methodName) => {
// @ts-expect-error Purposely passing in empty object for options
await expect(s1ActionsClient[methodName]({})).rejects.toBeInstanceOf(
ResponseActionsNotSupportedError
);
}
);
it.each(['killProcess', 'suspendProcess', 'runningProcesses', 'execute', 'upload'] as Array<
keyof ResponseActionsClient
>)('should throw an un-supported error for %s', async (methodName) => {
// @ts-expect-error Purposely passing in empty object for options
await expect(s1ActionsClient[methodName]({})).rejects.toBeInstanceOf(
ResponseActionsNotSupportedError
);
});
it('should error if multiple agent ids are received', async () => {
const payload = createS1IsolationOptions();
@ -560,4 +555,218 @@ describe('SentinelOneActionsClient class', () => {
});
});
});
describe('#getFile()', () => {
let getFileReqOptions: ResponseActionGetFileRequestBody;
beforeEach(() => {
// @ts-expect-error readonly prop assignment
classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled =
true;
getFileReqOptions = responseActionsClientMock.createGetFileOptions();
});
it('should error if feature flag is not enabled', async () => {
// @ts-expect-error readonly prop assignment
classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled =
false;
await expect(s1ActionsClient.getFile(getFileReqOptions)).rejects.toHaveProperty(
'message',
'get-file not supported for sentinel_one agent type. Feature disabled'
);
});
it('should call the fetch agent files connector method with expected params', async () => {
await s1ActionsClient.getFile(getFileReqOptions);
expect(connectorActionsMock.execute).toHaveBeenCalledWith({
params: {
subAction: SUB_ACTION.FETCH_AGENT_FILES,
subActionParams: {
agentUUID: '1-2-3',
files: [getFileReqOptions.parameters.path],
zipPassCode: SENTINEL_ONE_ZIP_PASSCODE,
},
},
});
});
it('should throw if sentinelone api generated an error (manual mode)', async () => {
const executeMockFn = (connectorActionsMock.execute as jest.Mock).getMockImplementation();
const err = new Error('oh oh');
(connectorActionsMock.execute as jest.Mock).mockImplementation(async (options) => {
if (options.params.subAction === SUB_ACTION.FETCH_AGENT_FILES) {
throw err;
}
return executeMockFn!(options);
});
await expect(s1ActionsClient.getFile(getFileReqOptions)).rejects.toEqual(err);
await expect(connectorActionsMock.execute).not.toHaveBeenCalledWith({
params: expect.objectContaining({
subAction: SUB_ACTION.GET_ACTIVITIES,
}),
});
});
it('should create failed response action when calling sentinelone api generated an error (automated mode)', async () => {
const subActionsClient = sentinelOneMock.createConnectorActionsClient();
classConstructorOptions = sentinelOneMock.createConstructorOptions();
classConstructorOptions.isAutomated = true;
classConstructorOptions.connectorActions =
responseActionsClientMock.createNormalizedExternalConnectorClient(subActionsClient);
connectorActionsMock = classConstructorOptions.connectorActions;
// @ts-expect-error readonly prop assignment
classConstructorOptions.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled =
true;
s1ActionsClient = new SentinelOneActionsClient(classConstructorOptions);
const executeMockFn = (subActionsClient.execute as jest.Mock).getMockImplementation();
const err = new Error('oh oh');
(subActionsClient.execute as jest.Mock).mockImplementation(async (options) => {
if (options.params.subAction === SUB_ACTION.FETCH_AGENT_FILES) {
throw err;
}
return executeMockFn!.call(SentinelOneActionsClient.prototype, options);
});
await expect(s1ActionsClient.getFile(getFileReqOptions)).resolves.toBeTruthy();
expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith(
{
document: {
'@timestamp': expect.any(String),
EndpointActions: {
action_id: expect.any(String),
data: {
command: 'get-file',
comment: 'test comment',
parameters: {
path: '/some/file',
},
hosts: {
'1-2-3': {
name: 'sentinelone-1460',
},
},
},
expiration: expect.any(String),
input_type: 'sentinel_one',
type: 'INPUT_ACTION',
},
agent: { id: ['1-2-3'] },
user: { id: 'foo' },
error: {
// The error message here is "not supported" because `get-file` is not currently supported
// for automated response actions. if that changes in the future the message below should
// be changed to `err.message` (`err` is defined and used in the mock setup above)
message: 'Action [get-file] not supported',
},
meta: {
agentId: '1845174760470303882',
agentUUID: '1-2-3',
hostName: 'sentinelone-1460',
},
},
index: ENDPOINT_ACTIONS_INDEX,
refresh: 'wait_for',
},
{ meta: true }
);
});
it('should query for the activity log entry record after successful submit of action', async () => {
await s1ActionsClient.getFile(getFileReqOptions);
expect(connectorActionsMock.execute).toHaveBeenNthCalledWith(3, {
params: {
subAction: SUB_ACTION.GET_ACTIVITIES,
subActionParams: {
activityTypes: '81',
limit: 1,
sortBy: 'createdAt',
sortOrder: 'asc',
// eslint-disable-next-line @typescript-eslint/naming-convention
createdAt__gte: expect.any(String),
agentIds: '1845174760470303882',
},
},
});
});
it('should create action request ES document with expected meta content', async () => {
await s1ActionsClient.getFile(getFileReqOptions);
expect(classConstructorOptions.esClient.index).toHaveBeenCalledWith(
{
document: {
'@timestamp': expect.any(String),
EndpointActions: {
action_id: expect.any(String),
data: {
command: 'get-file',
comment: 'test comment',
parameters: {
path: '/some/file',
},
hosts: {
'1-2-3': {
name: 'sentinelone-1460',
},
},
},
expiration: expect.any(String),
input_type: 'sentinel_one',
type: 'INPUT_ACTION',
},
agent: { id: ['1-2-3'] },
user: { id: 'foo' },
meta: {
agentId: '1845174760470303882',
agentUUID: '1-2-3',
hostName: 'sentinelone-1460',
activityId: '1929937418124016884',
commandBatchUuid: '7011777f-77e7-4a01-a674-e5f767808895',
},
},
index: ENDPOINT_ACTIONS_INDEX,
refresh: 'wait_for',
},
{ meta: true }
);
});
it('should return action details', async () => {
await expect(s1ActionsClient.getFile(getFileReqOptions)).resolves.toEqual(
// Only validating that a ActionDetails is returned. The data is mocked,
// so it does not make sense to validate the property values
{
action: expect.any(String),
agentState: expect.any(Object),
agentType: expect.any(String),
agents: expect.any(Array),
command: expect.any(String),
comment: expect.any(String),
createdBy: expect.any(String),
hosts: expect.any(Object),
id: expect.any(String),
isCompleted: expect.any(Boolean),
isExpired: expect.any(Boolean),
outputs: expect.any(Object),
startedAt: expect.any(String),
status: expect.any(String),
wasSuccessful: expect.any(Boolean),
}
);
});
it('should update cases', async () => {
await s1ActionsClient.getFile(
responseActionsClientMock.createGetFileOptions({ case_ids: ['case-1'] })
);
expect(classConstructorOptions.casesClient?.attachments.bulkCreate).toHaveBeenCalled();
});
});
});

View file

@ -14,15 +14,18 @@ import type { ActionTypeExecutorResult } from '@kbn/actions-plugin/common';
import type {
SentinelOneGetAgentsParams,
SentinelOneGetAgentsResponse,
SentinelOneGetActivitiesParams,
SentinelOneGetActivitiesResponse,
} from '@kbn/stack-connectors-plugin/common/sentinelone/types';
import type {
QueryDslQueryContainer,
SearchHit,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { SENTINEL_ONE_ZIP_PASSCODE } from '../../../../../../common/endpoint/service/response_actions/sentinel_one';
import type {
NormalizedExternalConnectorClientExecuteOptions,
NormalizedExternalConnectorClient,
NormalizedExternalConnectorClientExecuteOptions,
} from '../lib/normalized_external_connector_client';
import { SENTINEL_ONE_ACTIVITY_INDEX } from '../../../../../../common';
import { catchAndWrapError } from '../../../../utils';
@ -42,12 +45,18 @@ import type {
EndpointActionResponseDataOutput,
LogsEndpointAction,
LogsEndpointActionResponse,
ResponseActionGetFileOutputContent,
ResponseActionGetFileParameters,
SentinelOneActionRequestCommonMeta,
SentinelOneActivityEsDoc,
SentinelOneGetFileRequestMeta,
SentinelOneIsolationRequestMeta,
SentinelOneIsolationResponseMeta,
} from '../../../../../../common/endpoint/types';
import type { IsolationRouteRequestBody } from '../../../../../../common/api/endpoint';
import type {
IsolationRouteRequestBody,
ResponseActionGetFileRequestBody,
} from '../../../../../../common/api/endpoint';
import type {
ResponseActionsClientOptions,
ResponseActionsClientValidateRequestResponse,
@ -69,6 +78,48 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
connectorActions.setup(SENTINELONE_CONNECTOR_ID);
}
private async handleResponseActionCreation<
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
TMeta extends {} = {}
>(
reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
TParameters,
TOutputContent,
Partial<TMeta>
>
): Promise<{
actionEsDoc: LogsEndpointAction<TParameters, TOutputContent, TMeta>;
actionDetails: ActionDetails<TOutputContent, TParameters>;
}> {
const actionRequestDoc = await this.writeActionRequestToEndpointIndex<
TParameters,
TOutputContent,
TMeta
>(reqIndexOptions);
await this.updateCases({
command: reqIndexOptions.command,
caseIds: reqIndexOptions.case_ids,
alertIds: reqIndexOptions.alert_ids,
actionId: actionRequestDoc.EndpointActions.action_id,
hosts: reqIndexOptions.endpoint_ids.map((agentId) => {
return {
hostId: agentId,
hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '',
};
}),
comment: reqIndexOptions.comment,
});
return {
actionEsDoc: actionRequestDoc,
actionDetails: await this.fetchActionDetails<ActionDetails<TOutputContent, TParameters>>(
actionRequestDoc.EndpointActions.action_id
),
};
}
protected async writeActionRequestToEndpointIndex<
TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes,
TOutputContent extends EndpointActionResponseDataOutput = EndpointActionResponseDataOutput,
@ -77,7 +128,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
actionRequest: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
TParameters,
TOutputContent,
TMeta
Partial<TMeta> // Partial<> because the common Meta properties are actually set in this method for all requests
>
): Promise<
LogsEndpointAction<TParameters, TOutputContent, TMeta & SentinelOneActionRequestCommonMeta>
@ -110,10 +161,10 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
* Sends actions to SentinelOne directly (via Connector)
* @private
*/
private async sendAction(
private async sendAction<T = unknown>(
actionType: SUB_ACTION,
actionParams: object
): Promise<ActionTypeExecutorResult<unknown>> {
): Promise<ActionTypeExecutorResult<T>> {
const executeOptions: Parameters<typeof this.connectorActionsClient.execute>[0] = {
params: {
subAction: actionType,
@ -141,7 +192,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
this.log.debug(`Response:\n${stringify(actionSendResponse)}`);
return actionSendResponse;
return actionSendResponse as ActionTypeExecutorResult<T>;
}
/** Gets agent details directly from SentinelOne */
@ -174,7 +225,7 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
s1ApiResponse = response.data;
} catch (err) {
throw new ResponseActionsClientError(
`Error while attempting to retrieve SentinelOne host with agent id [${agentUUID}]`,
`Error while attempting to retrieve SentinelOne host with agent id [${agentUUID}]: ${err.message}`,
500,
err
);
@ -236,21 +287,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
}
}
const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions);
await this.updateCases({
command: reqIndexOptions.command,
caseIds: reqIndexOptions.case_ids,
alertIds: reqIndexOptions.alert_ids,
actionId: actionRequestDoc.EndpointActions.action_id,
hosts: actionRequest.endpoint_ids.map((agentId) => {
return {
hostId: agentId,
hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '',
};
}),
comment: reqIndexOptions.comment,
});
const { actionDetails, actionEsDoc: actionRequestDoc } =
await this.handleResponseActionCreation(reqIndexOptions);
if (
!actionRequestDoc.error &&
@ -263,9 +301,11 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
command: actionRequestDoc.EndpointActions.data.command,
},
});
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
}
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
return actionDetails;
}
async release(
@ -300,21 +340,8 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
}
}
const actionRequestDoc = await this.writeActionRequestToEndpointIndex(reqIndexOptions);
await this.updateCases({
command: reqIndexOptions.command,
caseIds: reqIndexOptions.case_ids,
alertIds: reqIndexOptions.alert_ids,
actionId: actionRequestDoc.EndpointActions.action_id,
hosts: actionRequest.endpoint_ids.map((agentId) => {
return {
hostId: agentId,
hostname: actionRequestDoc.EndpointActions.data.hosts?.[agentId].name ?? '',
};
}),
comment: reqIndexOptions.comment,
});
const { actionDetails, actionEsDoc: actionRequestDoc } =
await this.handleResponseActionCreation(reqIndexOptions);
if (
!actionRequestDoc.error &&
@ -327,9 +354,110 @@ export class SentinelOneActionsClient extends ResponseActionsClientImpl {
command: actionRequestDoc.EndpointActions.data.command,
},
});
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
}
return this.fetchActionDetails(actionRequestDoc.EndpointActions.action_id);
return actionDetails;
}
async getFile(
actionRequest: ResponseActionGetFileRequestBody,
options?: CommonResponseActionMethodOptions
): Promise<ActionDetails<ResponseActionGetFileOutputContent, ResponseActionGetFileParameters>> {
if (
!this.options.endpointService.experimentalFeatures.responseActionsSentinelOneGetFileEnabled
) {
throw new ResponseActionsClientError(
`get-file not supported for ${this.agentType} agent type. Feature disabled`,
400
);
}
const reqIndexOptions: ResponseActionsClientWriteActionRequestToEndpointIndexOptions<
ResponseActionGetFileParameters,
ResponseActionGetFileOutputContent,
Partial<SentinelOneGetFileRequestMeta>
> = {
...actionRequest,
...this.getMethodOptions(options),
command: 'get-file',
};
if (!reqIndexOptions.error) {
let error = (await this.validateRequest(reqIndexOptions)).error;
const timestamp = new Date().toISOString();
if (!error) {
try {
await this.sendAction(SUB_ACTION.FETCH_AGENT_FILES, {
agentUUID: actionRequest.endpoint_ids[0],
files: [actionRequest.parameters.path],
zipPassCode: SENTINEL_ONE_ZIP_PASSCODE,
});
} catch (err) {
error = err;
}
}
reqIndexOptions.error = error?.message;
if (!this.options.isAutomated && error) {
throw error;
}
if (!error) {
const { id: agentId } = await this.getAgentDetails(actionRequest.endpoint_ids[0]);
const activitySearchCriteria: SentinelOneGetActivitiesParams = {
// Activity type for fetching a file from a host machine in SentinelOne:
// {
// "id": 81
// "action": "User Requested Fetch Files",
// "descriptionTemplate": "The management user {{ username }} initiated a fetch file command to the agent {{ computer_name }} ({{ external_ip }}).",
// },
activityTypes: '81',
limit: 1,
sortBy: 'createdAt',
sortOrder: 'asc',
// eslint-disable-next-line @typescript-eslint/naming-convention
createdAt__gte: timestamp,
agentIds: agentId,
};
// Fetch the Activity log entry for this get-file request and store needed data
const activityLogSearchResponse = await this.sendAction<
SentinelOneGetActivitiesResponse<{ commandBatchUuid: string }>
>(SUB_ACTION.GET_ACTIVITIES, activitySearchCriteria);
this.log.debug(
`Search of activity log with:\n${stringify(
activitySearchCriteria
)}\n returned:\n${stringify(activityLogSearchResponse.data)}`
);
if (activityLogSearchResponse.data?.data.length) {
const activityLogItem = activityLogSearchResponse.data?.data[0];
reqIndexOptions.meta = {
commandBatchUuid: activityLogItem?.data.commandBatchUuid,
activityId: activityLogItem?.id,
};
} else {
this.log.warn(
`Unable to find a fetch file command entry in SentinelOne activity log. May be unable to complete response action`
);
}
}
}
return (
await this.handleResponseActionCreation<
ResponseActionGetFileParameters,
ResponseActionGetFileOutputContent,
SentinelOneGetFileRequestMeta
>(reqIndexOptions)
).actionDetails;
}
async processPendingActions({