[Security Solution][Endpoint] Agent status api support for Microsoft Defender for Endpoint hosts (#205817)

## Summary

### Stack Connectors changes

- Added new method to the Microsoft Defender for Endpoint connector to
retrieve list of Machines

### Security Solution

- Added support for retrieving the status of Microsoft Defender agents
This commit is contained in:
Paul Tavares 2025-01-10 14:20:46 -05:00 committed by GitHub
parent 221f1b100f
commit d8918077d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 498 additions and 5 deletions

View file

@ -11,6 +11,7 @@ export const MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID = '.microsoft_defender_end
export enum MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION {
TEST_CONNECTOR = 'testConnector',
GET_AGENT_DETAILS = 'getAgentDetails',
GET_AGENT_LIST = 'getAgentList',
ISOLATE_HOST = 'isolateHost',
RELEASE_HOST = 'releaseHost',
GET_ACTIONS = 'getActions',

View file

@ -37,6 +37,105 @@ export const AgentDetailsParamsSchema = schema.object({
id: schema.string({ minLength: 1 }),
});
const MachineHealthStatusSchema = schema.oneOf([
schema.literal('Active'),
schema.literal('Inactive'),
schema.literal('ImpairedCommunication'),
schema.literal('NoSensorData'),
schema.literal('NoSensorDataImpairedCommunication'),
schema.literal('Unknown'),
]);
export const AgentListParamsSchema = schema.object({
computerDnsName: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
id: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
version: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
deviceValue: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
aaDeviceId: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
machineTags: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
lastSeen: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
exposureLevel: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
onboardingStatus: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
lastIpAddress: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
healthStatus: schema.maybe(
schema.oneOf([
MachineHealthStatusSchema,
schema.arrayOf(MachineHealthStatusSchema, { minSize: 1 }),
])
),
osPlatform: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
riskScore: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
rbacGroupId: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
page: schema.maybe(schema.number({ min: 1, defaultValue: 1 })),
pageSize: schema.maybe(schema.number({ min: 1, max: 1000, defaultValue: 20 })),
});
export const IsolateHostParamsSchema = schema.object({
id: schema.string({ minLength: 1 }),
comment: schema.string({ minLength: 1 }),

View file

@ -17,6 +17,7 @@ import {
TestConnectorParamsSchema,
AgentDetailsParamsSchema,
GetActionsParamsSchema,
AgentListParamsSchema,
} from './schema';
export type MicrosoftDefenderEndpointConfig = TypeOf<typeof MicrosoftDefenderEndpointConfigSchema>;
@ -35,6 +36,18 @@ export interface MicrosoftDefenderEndpointTestConnector {
export type MicrosoftDefenderEndpointAgentDetailsParams = TypeOf<typeof AgentDetailsParamsSchema>;
export type MicrosoftDefenderEndpointAgentListParams = TypeOf<typeof AgentListParamsSchema>;
export interface MicrosoftDefenderEndpointAgentListResponse {
'@odata.context': string;
'@odata.count'?: number;
/** If value is `-1`, then API did not provide a total count */
total: number;
page: number;
pageSize: number;
value: MicrosoftDefenderEndpointMachine[];
}
export type MicrosoftDefenderEndpointGetActionsParams = TypeOf<typeof GetActionsParamsSchema>;
export interface MicrosoftDefenderEndpointGetActionsResponse {

View file

@ -205,4 +205,31 @@ describe('Microsoft Defender for Endpoint Connector', () => {
}
);
});
describe('#getAgentList()', () => {
it('should return expected response', async () => {
await expect(
connectorMock.instanceMock.getAgentList({ id: '1-2-3' }, connectorMock.usageCollector)
).resolves.toEqual({
'@odata.context': 'https://api-us3.securitycenter.microsoft.com/api/$metadata#Machines',
'@odata.count': 1,
page: 1,
pageSize: 20,
total: 1,
value: [expect.any(Object)],
});
});
it('should call Microsoft API with expected query params', async () => {
await connectorMock.instanceMock.getAgentList({ id: '1-2-3' }, connectorMock.usageCollector);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://api.mock__microsoft.com/api/machines',
params: { $count: true, $filter: 'id eq 1-2-3', $top: 20 },
}),
connectorMock.usageCollector
);
});
});
});

View file

@ -18,6 +18,7 @@ import {
MicrosoftDefenderEndpointDoNotValidateResponseSchema,
GetActionsParamsSchema,
AgentDetailsParamsSchema,
AgentListParamsSchema,
} from '../../../common/microsoft_defender_endpoint/schema';
import {
MicrosoftDefenderEndpointAgentDetailsParams,
@ -32,6 +33,8 @@ import {
MicrosoftDefenderEndpointTestConnector,
MicrosoftDefenderEndpointGetActionsParams,
MicrosoftDefenderEndpointGetActionsResponse,
MicrosoftDefenderEndpointAgentListParams,
MicrosoftDefenderEndpointAgentListResponse,
} from '../../../common/microsoft_defender_endpoint/types';
export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
@ -70,6 +73,11 @@ export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
method: 'getAgentDetails',
schema: AgentDetailsParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_LIST,
method: 'getAgentList',
schema: AgentListParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST,
@ -243,6 +251,30 @@ export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
);
}
public async getAgentList(
{ page = 1, pageSize = 20, ...filter }: MicrosoftDefenderEndpointAgentListParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointAgentListResponse> {
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/get-machines
// OData usage reference: https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-odata-samples
const response = await this.fetchFromMicrosoft<MicrosoftDefenderEndpointAgentListResponse>(
{
url: `${this.urls.machines}`,
method: 'GET',
params: this.buildODataUrlParams({ filter, page, pageSize }),
},
connectorUsageCollector
);
return {
...response,
page,
pageSize,
total: response['@odata.count'] ?? -1,
};
}
public async isolateHost(
{ id, comment }: MicrosoftDefenderEndpointIsolateHostParams,
connectorUsageCollector: ConnectorUsageCollector

View file

@ -78,6 +78,14 @@ const createMicrosoftDefenderConnectorMock = (): CreateMicrosoftDefenderConnecto
'@odata.count': 1,
value: [createMicrosoftMachineAction()],
}),
// Machine List
[`${apiUrl}/api/machines`]: () =>
createAxiosResponseMock({
'@odata.context': 'https://api-us3.securitycenter.microsoft.com/api/$metadata#Machines',
'@odata.count': 1,
value: [createMicrosoftMachineMock()],
}),
};
instanceMock.request.mockImplementation(

View file

@ -71,9 +71,10 @@ describe('Agent Status API route handler', () => {
});
it.each`
agentType | featureFlag
${'sentinel_one'} | ${'responseActionsSentinelOneV1Enabled'}
${'crowdstrike'} | ${'responseActionsCrowdstrikeManualHostIsolationEnabled'}
agentType | featureFlag
${'sentinel_one'} | ${'responseActionsSentinelOneV1Enabled'}
${'crowdstrike'} | ${'responseActionsCrowdstrikeManualHostIsolationEnabled'}
${'microsoft_defender_endpoint'} | ${'responseActionsMSDefenderEndpointEnabled'}
`(
'should error if the $agentType feature flag ($featureFlag) is turned off',
async ({
@ -102,6 +103,10 @@ describe('Agent Status API route handler', () => {
it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept agent type of %s', async (agentType) => {
httpRequestMock.query.agentType = agentType;
apiTestSetup.endpointAppContextMock.experimentalFeatures = {
...apiTestSetup.endpointAppContextMock.experimentalFeatures,
responseActionsMSDefenderEndpointEnabled: true,
};
await apiTestSetup
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);

View file

@ -74,7 +74,10 @@ export const getAgentStatusRouteHandler = (
(agentType === 'sentinel_one' &&
!endpointContext.experimentalFeatures.responseActionsSentinelOneV1Enabled) ||
(agentType === 'crowdstrike' &&
!endpointContext.experimentalFeatures.responseActionsCrowdstrikeManualHostIsolationEnabled)
!endpointContext.experimentalFeatures
.responseActionsCrowdstrikeManualHostIsolationEnabled) ||
(agentType === 'microsoft_defender_endpoint' &&
!endpointContext.experimentalFeatures.responseActionsMSDefenderEndpointEnabled)
) {
return errorHandler(
logger,

View file

@ -12,6 +12,7 @@ import {
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
import type {
MicrosoftDefenderEndpointAgentListResponse,
MicrosoftDefenderEndpointGetActionsResponse,
MicrosoftDefenderEndpointMachine,
MicrosoftDefenderEndpointMachineAction,
@ -59,6 +60,11 @@ const createMsConnectorActionsClientMock = (): ActionsClientMock => {
data: createMicrosoftMachineMock(),
});
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_LIST:
return responseActionsClientMock.createConnectorActionExecuteResponse({
data: createMicrosoftGetMachineListApiResponseMock(),
});
case MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST:
return responseActionsClientMock.createConnectorActionExecuteResponse({
data: createMicrosoftMachineActionMock({ type: 'Isolate' }),
@ -159,9 +165,23 @@ const createMicrosoftGetActionsApiResponseMock =
};
};
const createMicrosoftGetMachineListApiResponseMock =
(): MicrosoftDefenderEndpointAgentListResponse => {
return {
'@odata.context': 'some-context',
'@odata.count': 1,
total: 1,
page: 1,
pageSize: 0,
value: [createMicrosoftMachineMock()],
};
};
export const microsoftDefenderMock = {
createConstructorOptions: createMsDefenderClientConstructorOptionsMock,
createMsConnectorActionsClient: createMsConnectorActionsClientMock,
createMachineAction: createMicrosoftMachineActionMock,
createMachine: createMicrosoftMachineMock,
createGetActionsApiResponse: createMicrosoftGetActionsApiResponseMock,
createMicrosoftGetMachineListApiResponse: createMicrosoftGetMachineListApiResponseMock,
};

View file

@ -76,8 +76,10 @@ export const getPendingActionsSummary = async (
setActionAsPending(unExpiredAction.command);
} else if (
unExpiredAction.wasSuccessful &&
(unExpiredAction.command === 'isolate' || unExpiredAction.command === 'unisolate')
(unExpiredAction.command === 'isolate' || unExpiredAction.command === 'unisolate') &&
unExpiredAction.agentType === 'endpoint'
) {
// For Elastic Defend (endpoint):
// For Isolate and Un-Isolate, we want to ensure that the isolation status being reported in the
// endpoint metadata was received after the action was completed. This is to ensure that the
// isolation status being reported in the UI remains as accurate as possible.

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MicrosoftDefenderEndpointAgentStatusClient } from './microsoft_defender_endpoint';
import { CrowdstrikeAgentStatusClient } from './crowdstrike/crowdstrike_agent_status_client';
import { SentinelOneAgentStatusClient } from './sentinel_one/sentinel_one_agent_status_client';
import type { AgentStatusClientInterface } from './lib/types';
@ -30,6 +31,8 @@ export const getAgentStatusClient = (
return new SentinelOneAgentStatusClient(constructorOptions);
case 'crowdstrike':
return new CrowdstrikeAgentStatusClient(constructorOptions);
case 'microsoft_defender_endpoint':
return new MicrosoftDefenderEndpointAgentStatusClient(constructorOptions);
default:
throw new UnsupportedAgentTypeError(
`Agent type [${agentType}] does not support agent status`

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './microsoft_defender_endpoint_agent_status_client';

View file

@ -0,0 +1,155 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getPendingActionsSummary as _getPendingActionsSummary } from '../../..';
import { createMockEndpointAppContextService } from '../../../../mocks';
import { MicrosoftDefenderEndpointAgentStatusClient } from './microsoft_defender_endpoint_agent_status_client';
import { microsoftDefenderMock } from '../../../actions/clients/microsoft/defender/endpoint/mocks';
import type { AgentStatusClientOptions } from '../lib/base_agent_status_client';
import { HostStatus } from '../../../../../../common/endpoint/types';
import { responseActionsClientMock } from '../../../actions/clients/mocks';
jest.mock('../../../actions/pending_actions_summary', () => {
const realModule = jest.requireActual('../../../actions/pending_actions_summary');
return {
...realModule,
getPendingActionsSummary: jest.fn(realModule.getPendingActionsSummary),
};
});
const getPendingActionsSummaryMock = _getPendingActionsSummary as jest.Mock;
describe('Microsoft Defender Agent Status client', () => {
let clientConstructorOptions: AgentStatusClientOptions;
let msAgentStatusClientMock: MicrosoftDefenderEndpointAgentStatusClient;
beforeEach(() => {
const endpointAppContextServiceMock = createMockEndpointAppContextService();
const soClient = endpointAppContextServiceMock.savedObjects.createInternalScopedSoClient({
readonly: false,
});
getPendingActionsSummaryMock.mockResolvedValue([
{
agent_id: '1-2-3',
pending_actions: { isolate: 1 },
},
]);
clientConstructorOptions = {
endpointService: endpointAppContextServiceMock,
connectorActionsClient: microsoftDefenderMock.createMsConnectorActionsClient(),
esClient: endpointAppContextServiceMock.getInternalEsClient(),
soClient,
};
msAgentStatusClientMock = new MicrosoftDefenderEndpointAgentStatusClient(
clientConstructorOptions
);
});
afterEach(() => {
getPendingActionsSummaryMock.mockReset();
});
it('should error if instantiated with no Connector Actions Client', () => {
clientConstructorOptions.connectorActionsClient = undefined;
expect(() => new MicrosoftDefenderEndpointAgentStatusClient(clientConstructorOptions)).toThrow(
'connectorActionsClient is required to create an instance of MicrosoftDefenderEndpointAgentStatusClient'
);
});
it('should call connector to get list of machines from MS using the IDs passed in', async () => {
await msAgentStatusClientMock.getAgentStatuses(['1-2-3', 'foo']);
expect(clientConstructorOptions.connectorActionsClient?.execute).toHaveBeenCalledWith(
expect.objectContaining({
params: {
subAction: 'getAgentList',
subActionParams: { id: ['1-2-3', 'foo'] },
},
})
);
});
it('should retrieve a list of pending response actions with the IDs that were passed in', async () => {
await msAgentStatusClientMock.getAgentStatuses(['1-2-3', 'foo']);
expect(getPendingActionsSummaryMock).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
['1-2-3', 'foo']
);
});
it('should return the expected agent status records', async () => {
await expect(msAgentStatusClientMock.getAgentStatuses(['1-2-3', 'foo'])).resolves.toEqual({
'1-2-3': {
agentId: '1-2-3',
agentType: 'microsoft_defender_endpoint',
found: true,
isolated: false,
lastSeen: '2018-08-02T14:55:03.7791856Z',
pendingActions: {
isolate: 1,
},
status: 'healthy',
},
foo: {
agentId: 'foo',
agentType: 'microsoft_defender_endpoint',
found: false,
isolated: false,
lastSeen: '',
pendingActions: {},
status: 'unenrolled',
},
});
});
it.each`
msHealthStatus | expectedAgentStatus
${'Active'} | ${HostStatus.HEALTHY}
${'Inactive'} | ${HostStatus.INACTIVE}
${'ImpairedCommunication'} | ${HostStatus.UNHEALTHY}
${'NoSensorData'} | ${HostStatus.UNHEALTHY}
${'NoSensorDataImpairedCommunication'} | ${HostStatus.UNHEALTHY}
${'Unknown'} | ${HostStatus.UNENROLLED}
`(
'should correctly map MS machine healthStatus of $msHealthStatus to agent status $expectedAgentStatus',
async ({ msHealthStatus, expectedAgentStatus }) => {
const priorExecuteMock = (
clientConstructorOptions.connectorActionsClient?.execute as jest.Mock
).getMockImplementation();
(clientConstructorOptions.connectorActionsClient?.execute as jest.Mock).mockImplementation(
async (options) => {
if (options.params.subAction === 'getAgentList') {
const machineListResponse =
microsoftDefenderMock.createMicrosoftGetMachineListApiResponse();
machineListResponse.value[0].healthStatus = msHealthStatus;
return responseActionsClientMock.createConnectorActionExecuteResponse({
data: machineListResponse,
});
}
if (priorExecuteMock) {
return priorExecuteMock(options);
}
}
);
await expect(msAgentStatusClientMock.getAgentStatuses(['1-2-3'])).resolves.toEqual({
'1-2-3': expect.objectContaining({
status: expectedAgentStatus,
}),
});
}
);
});

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
import { keyBy } from 'lodash';
import type {
MicrosoftDefenderEndpointAgentListResponse,
MicrosoftDefenderEndpointMachine,
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/types';
import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants';
import { AgentStatusClientError } from '../errors';
import { getPendingActionsSummary, NormalizedExternalConnectorClient } from '../../..';
import type { AgentStatusClientOptions } from '../lib/base_agent_status_client';
import { AgentStatusClient } from '../lib/base_agent_status_client';
import type { AgentStatusRecords } from '../../../../../../common/endpoint/types';
import { HostStatus } from '../../../../../../common/endpoint/types';
export class MicrosoftDefenderEndpointAgentStatusClient extends AgentStatusClient {
protected readonly agentType: ResponseActionAgentType = 'microsoft_defender_endpoint';
protected readonly connectorActions: NormalizedExternalConnectorClient;
constructor(options: AgentStatusClientOptions) {
super(options);
if (!options.connectorActionsClient) {
throw new AgentStatusClientError(
'connectorActionsClient is required to create an instance of MicrosoftDefenderEndpointAgentStatusClient'
);
}
this.connectorActions = new NormalizedExternalConnectorClient(
options.connectorActionsClient,
this.log
);
this.connectorActions.setup(MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID);
}
protected getAgentStatusFromMachineHealthStatus(
healthStatus: MicrosoftDefenderEndpointMachine['healthStatus'] | undefined
): HostStatus {
// Definition of sensor health status can be found here:
// https://learn.microsoft.com/en-us/defender-endpoint/check-sensor-status
switch (healthStatus) {
case 'Active':
return HostStatus.HEALTHY;
case 'Inactive':
return HostStatus.INACTIVE;
case 'ImpairedCommunication':
case 'NoSensorData':
case 'NoSensorDataImpairedCommunication':
return HostStatus.UNHEALTHY;
default:
return HostStatus.UNENROLLED;
}
}
public async getAgentStatuses(agentIds: string[]): Promise<AgentStatusRecords> {
const esClient = this.options.esClient;
const metadataService = this.options.endpointService.getEndpointMetadataService();
try {
const [{ data: msMachineListResponse }, allPendingActions] = await Promise.all([
this.connectorActions.execute<MicrosoftDefenderEndpointAgentListResponse>({
params: {
subAction: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_LIST,
subActionParams: { id: agentIds },
},
}),
// Fetch pending actions summary
getPendingActionsSummary(esClient, metadataService, this.log, agentIds),
]);
const machinesById = keyBy(msMachineListResponse?.value ?? [], 'id');
const pendingActionsByAgentId = keyBy(allPendingActions, 'agent_id');
return agentIds.reduce<AgentStatusRecords>((acc, agentId) => {
const thisMachine = machinesById[agentId];
const thisAgentPendingActions = pendingActionsByAgentId[agentId];
acc[agentId] = {
agentId,
agentType: this.agentType,
found: !!thisMachine,
// Unfortunately, it does not look like MS Defender has a way to determine
// if a host is isolated or not via API, so we just set this to false
isolated: false,
lastSeen: thisMachine?.lastSeen ?? '',
status: this.getAgentStatusFromMachineHealthStatus(thisMachine?.healthStatus),
pendingActions: thisAgentPendingActions?.pending_actions ?? {},
};
return acc;
}, {});
} catch (err) {
const error = new AgentStatusClientError(
`Failed to fetch Microsoft Defender for Endpoint agent status for agentIds: [${agentIds}], failed with: ${err.message}`,
500,
err
);
this.log.error(error);
throw error;
}
}
}