mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
9ea94d10b6
commit
6b0e38ad4e
7 changed files with 479 additions and 62 deletions
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue