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