[8.x] [Stack Connectors][Microsoft Defender] Adds new connector for Microsoft Defender for Endpoint (#203183) (#206511)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Stack Connectors][Microsoft Defender] Adds new connector for
Microsoft Defender for Endpoint
(#203183)](https://github.com/elastic/kibana/pull/203183)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Paul
Tavares","email":"56442535+paul-tavares@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-07T15:25:27Z","message":"[Stack
Connectors][Microsoft Defender] Adds new connector for Microsoft
Defender for Endpoint (#203183)\n\n## Summary\r\n\r\n- New connector for
Microsoft Defender for Endpoint. To be used in\r\nsupport of Security
Solution Bi-Directional response
actions.","sha":"b1957ae20910b613b93eff379fafe86e76ca1044","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","v9.0.0","Team:Defend
Workflows","backport:prev-minor","v8.18.0"],"number":203183,"url":"https://github.com/elastic/kibana/pull/203183","mergeCommit":{"message":"[Stack
Connectors][Microsoft Defender] Adds new connector for Microsoft
Defender for Endpoint (#203183)\n\n## Summary\r\n\r\n- New connector for
Microsoft Defender for Endpoint. To be used in\r\nsupport of Security
Solution Bi-Directional response
actions.","sha":"b1957ae20910b613b93eff379fafe86e76ca1044"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/203183","number":203183,"mergeCommit":{"message":"[Stack
Connectors][Microsoft Defender] Adds new connector for Microsoft
Defender for Endpoint (#203183)\n\n## Summary\r\n\r\n- New connector for
Microsoft Defender for Endpoint. To be used in\r\nsupport of Security
Solution Bi-Directional response
actions.","sha":"b1957ae20910b613b93eff379fafe86e76ca1044"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Paul Tavares 2025-01-14 08:39:19 -05:00 committed by GitHub
parent cb7cd554ad
commit 91f01011d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1957 additions and 16 deletions

7
.github/CODEOWNERS vendored
View file

@ -1750,8 +1750,15 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/common/sentinelone @elastic/security-defend-workflows
/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/crowdstrike @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/common/crowdstrike @elastic/security-defend-workflows
/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/crowdstrike.ts @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/microsoft_defender_endpoint @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/microsoft_defender_endpoint @elastic/security-defend-workflows
/x-pack/platform/plugins/shared/stack_connectors/common/microsoft_defender_endpoint @elastic/security-defend-workflows
/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/microsoft_defender_endpoint.ts @elastic/security-defend-workflows
## Security Solution shared OAS schemas
/x-pack/solutions/security/plugins/security_solution/common/api/model @elastic/security-detection-rule-management @elastic/security-detection-engine

View file

@ -17,6 +17,7 @@ export const allowedExperimentalValues = Object.freeze({
crowdstrikeConnectorOn: true,
inferenceConnectorOn: true,
crowdstrikeConnectorRTROn: false,
microsoftDefenderEndpointOn: false,
});
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -0,0 +1,17 @@
/*
* 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 const MICROSOFT_DEFENDER_ENDPOINT_TITLE = 'Microsoft Defender for Endpoint';
export const MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID = '.microsoft_defender_endpoint';
export enum MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION {
TEST_CONNECTOR = 'testConnector',
GET_AGENT_DETAILS = 'getAgentDetails',
ISOLATE_HOST = 'isolateHost',
RELEASE_HOST = 'releaseHost',
GET_ACTIONS = 'getActions',
}

View file

@ -0,0 +1,132 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from './constants';
// ----------------------------------
// Connector setup schemas
// ----------------------------------
export const MicrosoftDefenderEndpointConfigSchema = schema.object({
clientId: schema.string({ minLength: 1 }),
tenantId: schema.string({ minLength: 1 }),
oAuthServerUrl: schema.string({ minLength: 1 }),
oAuthScope: schema.string({ minLength: 1 }),
apiUrl: schema.string({ minLength: 1 }),
});
export const MicrosoftDefenderEndpointSecretsSchema = schema.object({
clientSecret: schema.string({ minLength: 1 }),
});
// ----------------------------------
// Connector Methods
// ----------------------------------
export const MicrosoftDefenderEndpointDoNotValidateResponseSchema = schema.any();
export const MicrosoftDefenderEndpointBaseApiResponseSchema = schema.maybe(
schema.object({}, { unknowns: 'allow' })
);
export const TestConnectorParamsSchema = schema.object({});
export const AgentDetailsParamsSchema = schema.object({
id: schema.string({ minLength: 1 }),
});
export const IsolateHostParamsSchema = schema.object({
id: schema.string({ minLength: 1 }),
comment: schema.string({ minLength: 1 }),
});
export const ReleaseHostParamsSchema = schema.object({
id: schema.string({ minLength: 1 }),
comment: schema.string({ minLength: 1 }),
});
const MachineActionTypeSchema = schema.oneOf([
schema.literal('RunAntiVirusScan'),
schema.literal('Offboard'),
schema.literal('LiveResponse'),
schema.literal('CollectInvestigationPackage'),
schema.literal('Isolate'),
schema.literal('Unisolate'),
schema.literal('StopAndQuarantineFile'),
schema.literal('RestrictCodeExecution'),
schema.literal('UnrestrictCodeExecution'),
]);
const MachineActionStatusSchema = schema.oneOf([
schema.literal('Pending'),
schema.literal('InProgress'),
schema.literal('Succeeded'),
schema.literal('Failed'),
schema.literal('TimeOut'),
schema.literal('Cancelled'),
]);
export const GetActionsParamsSchema = schema.object({
id: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
status: schema.maybe(
schema.oneOf([
MachineActionStatusSchema,
schema.arrayOf(MachineActionStatusSchema, { minSize: 1 }),
])
),
machineId: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
type: schema.maybe(
schema.oneOf([MachineActionTypeSchema, schema.arrayOf(MachineActionTypeSchema, { minSize: 1 })])
),
requestor: schema.maybe(
schema.oneOf([
schema.string({ minLength: 1 }),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
])
),
creationDateTimeUtc: 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 })),
});
// ----------------------------------
// Connector Sub-Actions
// ----------------------------------
const TestConnectorSchema = schema.object({
subAction: schema.literal(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR),
subActionParams: TestConnectorParamsSchema,
});
const IsolateHostSchema = schema.object({
subAction: schema.literal(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST),
subActionParams: IsolateHostParamsSchema,
});
const ReleaseHostSchema = schema.object({
subAction: schema.literal(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RELEASE_HOST),
subActionParams: ReleaseHostParamsSchema,
});
export const MicrosoftDefenderEndpointActionParamsSchema = schema.oneOf([
TestConnectorSchema,
IsolateHostSchema,
ReleaseHostSchema,
]);

View file

@ -0,0 +1,177 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import {
MicrosoftDefenderEndpointSecretsSchema,
MicrosoftDefenderEndpointConfigSchema,
MicrosoftDefenderEndpointActionParamsSchema,
MicrosoftDefenderEndpointBaseApiResponseSchema,
IsolateHostParamsSchema,
ReleaseHostParamsSchema,
TestConnectorParamsSchema,
AgentDetailsParamsSchema,
GetActionsParamsSchema,
} from './schema';
export type MicrosoftDefenderEndpointConfig = TypeOf<typeof MicrosoftDefenderEndpointConfigSchema>;
export type MicrosoftDefenderEndpointSecrets = TypeOf<
typeof MicrosoftDefenderEndpointSecretsSchema
>;
export type MicrosoftDefenderEndpointBaseApiResponse = TypeOf<
typeof MicrosoftDefenderEndpointBaseApiResponseSchema
>;
export interface MicrosoftDefenderEndpointTestConnector {
results: string[];
}
export type MicrosoftDefenderEndpointAgentDetailsParams = TypeOf<typeof AgentDetailsParamsSchema>;
export type MicrosoftDefenderEndpointGetActionsParams = TypeOf<typeof GetActionsParamsSchema>;
export interface MicrosoftDefenderEndpointGetActionsResponse {
'@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: MicrosoftDefenderEndpointMachineAction[];
}
/**
* @see https://learn.microsoft.com/en-us/defender-endpoint/api/machine
*/
export interface MicrosoftDefenderEndpointMachine {
/** machine identity. */
id: string;
/** machine fully qualified name. */
computerDnsName: string;
/** First date and time where the machine was observed by Microsoft Defender for Endpoint. */
firstSeen: string;
/** Time and date of the last received full device report. A device typically sends a full report every 24 hours. NOTE: This property doesn't correspond to the last seen value in the UI. It pertains to the last device update. */
lastSeen: string;
/** Operating system platform. */
osPlatform: string;
/** Status of machine onboarding. Possible values are: onboarded, CanBeOnboarded, Unsupported, and InsufficientInfo. */
onboardingstatus: string;
/** Operating system processor. Use osArchitecture property instead. */
osProcessor: string;
/** Operating system Version. */
version: string;
/** Operating system build number. */
osBuild?: number;
/** Last IP on local NIC on the machine. */
lastIpAddress: string;
/** Last IP through which the machine accessed the internet. */
lastExternalIpAddress: string;
/** machine health status. Possible values are: Active, Inactive, ImpairedCommunication, NoSensorData, NoSensorDataImpairedCommunication, and Unknown. */
healthStatus:
| 'Active'
| 'Inactive'
| 'ImpairedCommunication'
| 'NoSensorData'
| 'NoSensorDataImpairedCommunication'
| 'Unknown';
/** Machine group Name. */
rbacGroupName: string;
/** Machine group ID. */
rbacGroupId: string;
/** Risk score as evaluated by Microsoft Defender for Endpoint. Possible values are: None, Informational, Low, Medium, and High. */
riskScore?: 'None' | 'Informational' | 'Low' | 'Medium' | 'High';
/** Microsoft Entra Device ID (when machine is Microsoft Entra joined). */
aadDeviceId?: string;
/** Set of machine tags. */
machineTags: string[];
/** Exposure level as evaluated by Microsoft Defender for Endpoint. Possible values are: None, Low, Medium, and High. */
exposureLevel?: 'None' | 'Low' | 'Medium' | 'High';
/** The value of the device. Possible values are: Normal, Low, and High. */
deviceValue?: 'Normal' | 'Low' | 'High';
/** Set of IpAddress objects. See Get machines API. */
ipAddresses: Array<{
ipAddress: string;
macAddress: string;
type: string;
operationalStatus: string;
}>;
/** Operating system architecture. Possible values are: 32-bit, 64-bit. Use this property instead of osProcessor. */
osArchitecture: string;
}
/**
* @see https://learn.microsoft.com/en-us/defender-endpoint/api/machineaction
*/
export interface MicrosoftDefenderEndpointMachineAction {
/** Identity of the Machine Action entity. */
id: string;
/** Type of the action. Possible values are: RunAntiVirusScan, Offboard, LiveResponse, CollectInvestigationPackage, Isolate, Unisolate, StopAndQuarantineFile, RestrictCodeExecution, and UnrestrictCodeExecution. */
type:
| 'RunAntiVirusScan'
| 'Offboard'
| 'LiveResponse'
| 'CollectInvestigationPackage'
| 'Isolate'
| 'Unisolate'
| 'StopAndQuarantineFile'
| 'RestrictCodeExecution'
| 'UnrestrictCodeExecution';
/** Scope of the action. Full or Selective for Isolation, Quick or Full for antivirus scan. */
scope?: string;
/** Identity of the person that executed the action. */
requestor: string;
/** Id the customer can submit in the request for custom correlation. */
externalID?: string;
/** The name of the user/application that submitted the action. */
requestSource: string;
/** Commands to run. Allowed values are PutFile, RunScript, GetFile. */
commands: Array<'PutFile' | 'RunScript' | 'GetFile'>;
/** Identity of the person that canceled the action. */
cancellationRequestor: string;
/** Comment that was written when issuing the action. */
requestorComment: string;
/** Comment that was written when canceling the action. */
cancellationComment: string;
/** Current status of the command. Possible values are: Pending, InProgress, Succeeded, Failed, TimeOut, and Cancelled. */
status: 'Pending' | 'InProgress' | 'Succeeded' | 'Failed' | 'TimeOut' | 'Cancelled';
/** ID of the machine on which the action was executed. */
machineId: string;
/** Name of the machine on which the action was executed. */
computerDnsName: string;
/** The date and time when the action was created. */
creationDateTimeUtc: string;
/** The date and time when the action was canceled. */
cancellationDateTimeUtc: string;
/** The last date and time when the action status was updated. */
lastUpdateDateTimeUtc: string;
/** Machine action title. */
title: string;
/** Contains two Properties. string fileIdentifier, Enum fileIdentifierType with the possible values: Sha1, Sha256, and Md5. */
relatedFileInfo?: { fileIdentifier: string; fileIdentifierType: 'Sha1' | 'Sha256' | 'Md5' };
errorResult?: number;
troubleshootInfo?: string;
}
export type MicrosoftDefenderEndpointTestConnectorParams = TypeOf<typeof TestConnectorParamsSchema>;
export type MicrosoftDefenderEndpointIsolateHostParams = TypeOf<typeof IsolateHostParamsSchema>;
export type MicrosoftDefenderEndpointReleaseHostParams = TypeOf<typeof ReleaseHostParamsSchema>;
export type MicrosoftDefenderEndpointActionParams = TypeOf<
typeof MicrosoftDefenderEndpointActionParamsSchema
>;
export interface MicrosoftDefenderEndpointApiTokenResponse {
token_type: 'bearer';
/** The amount of time that an access token is valid (in seconds NOT milliseconds). */
expires_in: number;
access_token: string;
}

View file

@ -30,3 +30,5 @@ export { CrowdstrikeLogo };
export { BEDROCK_CONNECTOR_ID } from '../../common/bedrock/constants';
export { BedrockLogo };
export { MicrosoftDefenderEndpointLogo } from '../connector_types/microsoft_defender_endpoint/logo';

View file

@ -7,6 +7,7 @@
import { ValidatedEmail, ValidateEmailAddressesOptions } from '@kbn/actions-plugin/common';
import { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public';
import { getMicrosoftDefenderEndpointConnectorType } from './microsoft_defender_endpoint';
import { getCasesWebhookConnectorType } from './cases_webhook';
import { getEmailConnectorType } from './email';
import { getIndexConnectorType } from './es_index';
@ -84,4 +85,7 @@ export function registerConnectorTypes({
if (ExperimentalFeaturesService.get().inferenceConnectorOn) {
connectorTypeRegistry.register(getInferenceConnectorType());
}
if (ExperimentalFeaturesService.get().microsoftDefenderEndpointOn) {
connectorTypeRegistry.register(getMicrosoftDefenderEndpointConnectorType());
}
}

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 { getConnectorType as getMicrosoftDefenderEndpointConnectorType } from './microsoft_defender_endpoint';

View file

@ -0,0 +1,35 @@
/*
* 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 React, { memo } from 'react';
import { EuiIcon, EuiIconProps } from '@elastic/eui';
const MicrosoftIconSvg = memo(() => {
return (
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 400 400"
>
<g fill="#0b61ce">
<path d="M180.3 52.4c-13.4 2.6-24.4 7.2-38 16C118.8 83.5 88.1 91.8 54.8 92c-4.2 0-7.8.4-8 1-.1.5 0 19.5.3 42.2.5 35.9.9 42.9 2.7 53.3 6.4 36 18.4 67.1 37.2 95.9 24.3 37.2 57.6 67 97.8 87.3 7.9 4 14.9 7.3 15.6 7.3 2.7 0 29.9-14.3 41.1-21.7C302 317.6 341.4 255.5 352 183.4c1.7-12.1 2-18.9 2-52.7V92h-6.7c-34.5-.1-64.2-8.2-90.3-24.6-19.2-12.1-30.4-15.4-53.5-16-10.8-.2-18.3.1-23.2 1zm38.2 20c9.9 2.2 17.4 5.3 26.4 10.9 13.7 8.6 15.2 9.4 24.1 13.2 18.8 8.1 37.8 12.8 57.5 14.4l8 .6-.1 27.5c-.2 43-4.9 68.2-18.9 101-20.4 47.8-60.4 90.2-106.6 113.1l-8.6 4.2-10.9-5.7c-50.5-26.7-88.8-70.3-108.2-123.1-11.1-30.1-14.2-49.6-14.5-91l-.2-26 5-.3c25.8-1.4 59.1-11.6 78.5-24 13.4-8.6 21.7-12.4 33.5-15.1 6.1-1.4 28.2-1.2 35 .3z" />
<path d="M189.5 92.6c-12.4 3-16.1 4.6-27.6 12-15.6 10.1-39.3 19.4-59.4 23.4-5.5 1.1-11.1 2.3-12.4 2.6l-2.4.5.6 21.2c.9 34.4 6.8 58.8 21.2 88.2 11.2 22.7 23.6 39.6 43.1 58.5 14.4 13.9 30.3 25.8 44 32.8l4.1 2.1 7.5-4.3c49.4-28.2 85.5-76.4 99.4-132.9 3.6-14.9 5.4-30.9 5.4-49.4 0-16 0-16.3-2.2-16.8-1.3-.3-5.9-1.2-10.3-2.1-22.8-4.4-44.6-13-63.5-25.1-14-8.8-19-10.5-33.5-10.9-6.6-.1-12.9-.1-14 .2zM227 173c-5.5 11-10 20.2-10 20.5 0 .2 9 .6 20.1.7l20.1.3-45.8 45.7c-25.2 25.2-45.9 45.6-46.1 45.4-.2-.2 6.4-13.9 14.6-30.5L195 225h-30.6l18-36 18.1-36H237l-10 20z" />
</g>
</svg>
);
});
MicrosoftIconSvg.displayName = 'MicrosoftIconSvg';
export const MicrosoftDefenderEndpointLogo = memo<Omit<EuiIconProps, 'type'>>((props) => {
return <EuiIcon {...props} type={MicrosoftIconSvg} />;
});
MicrosoftDefenderEndpointLogo.displayName = 'MicrosoftDefenderEndpointLogo';
// eslint-disable-next-line import/no-default-export
export { MicrosoftDefenderEndpointLogo as default };

View file

@ -0,0 +1,76 @@
/*
* 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 React from 'react';
import { type RenderResult } from '@testing-library/react';
import { ConnectorFormTestProvider, createAppMockRenderer } from '../lib/test_utils';
import MicrosoftDefenderEndpointActionConnectorFields from './microsoft_defender_endpoint_connector';
import { ActionConnectorFieldsProps } from '@kbn/alerts-ui-shared';
import { MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID } from '../../../common/microsoft_defender_endpoint/constants';
import { ConnectorFormSchema } from '@kbn/triggers-actions-ui-plugin/public';
describe('Microsoft Defender for Endpoint Connector UI', () => {
let renderProps: ActionConnectorFieldsProps;
let render: () => RenderResult;
let connectorFormProps: ConnectorFormSchema;
beforeEach(() => {
const appMockRenderer = createAppMockRenderer();
renderProps = {
readOnly: false,
isEdit: false,
registerPreSubmitValidator: jest.fn(),
};
connectorFormProps = {
id: 'test',
name: 'email',
isDeprecated: false,
actionTypeId: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
secrets: {
clientSecret: 'shhhh',
},
config: {
clientId: 'client-a',
tenantId: 'tenant-1',
oAuthServerUrl: 'https://t_e_s_t.com',
oAuthScope: 'some-scope',
apiUrl: 'https://api.t_e_s_t.com',
},
};
render = () => {
return appMockRenderer.render(
<ConnectorFormTestProvider connector={connectorFormProps}>
<MicrosoftDefenderEndpointActionConnectorFields {...renderProps} />
</ConnectorFormTestProvider>
);
};
});
it.each([
'config.clientId',
'config.tenantId',
'config.oAuthServerUrl',
'config.oAuthScope',
'config.apiUrl',
'secrets.clientSecret',
])('should display input for setting: %s', (field: string) => {
const { getByTestId } = render();
expect(getByTestId(`${field}-input`)).not.toBeNull();
});
it.each(['config.oAuthServerUrl', 'config.oAuthScope', 'config.apiUrl'])(
'should include default value for field: %s',
(field: string) => {
const { getByTestId } = render();
expect(getByTestId(`${field}-input`)).toHaveValue();
}
);
});

View file

@ -0,0 +1,65 @@
/*
* 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 { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import type {
ActionTypeModel as ConnectorTypeModel,
GenericValidationResult,
} from '@kbn/triggers-actions-ui-plugin/public';
import {
MICROSOFT_DEFENDER_ENDPOINT_TITLE,
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
} from '../../../common/microsoft_defender_endpoint/constants';
import type {
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointActionParams,
MicrosoftDefenderEndpointSecrets,
} from '../../../common/microsoft_defender_endpoint/types';
interface ValidationErrors {
subAction: string[];
}
export function getConnectorType(): ConnectorTypeModel<
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets,
MicrosoftDefenderEndpointActionParams
> {
return {
id: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
actionTypeTitle: MICROSOFT_DEFENDER_ENDPOINT_TITLE,
iconClass: lazy(() => import('./logo')),
isExperimental: true,
selectMessage: i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpointSecrets.config.selectMessageText',
{
defaultMessage: 'Execute response actions against Microsoft Defender Endpoint hosts',
}
),
validateParams: async (
actionParams: MicrosoftDefenderEndpointActionParams
): Promise<GenericValidationResult<ValidationErrors>> => {
const translations = await import('./translations');
const errors: ValidationErrors = {
subAction: [],
};
const { subAction } = actionParams;
// The internal "subAction" param should always be valid, ensure it is only if "subActionParams" are valid
if (!subAction) {
errors.subAction.push(translations.ACTION_REQUIRED);
} else if (!Object.values(MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION).includes(subAction)) {
errors.subAction.push(translations.INVALID_ACTION);
}
return { errors };
},
actionConnectorFields: lazy(() => import('./microsoft_defender_endpoint_connector')),
actionParamsFields: lazy(() => import('./microsoft_defender_endpoint_params')),
};
}

View file

@ -0,0 +1,71 @@
/*
* 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 React from 'react';
import {
ActionConnectorFieldsProps,
ConfigFieldSchema,
SecretsFieldSchema,
SimpleConnectorForm,
} from '@kbn/triggers-actions-ui-plugin/public';
import * as translations from './translations';
const configFormSchema: ConfigFieldSchema[] = [
{
id: 'clientId',
label: translations.CLIENT_ID_LABEL,
isRequired: true,
},
{
id: 'tenantId',
label: translations.TENANT_ID_LABEL,
isRequired: true,
},
{
id: 'oAuthServerUrl',
label: translations.OAUTH_URL_LABEL,
isRequired: true,
isUrlField: true,
defaultValue: 'https://login.microsoftonline.com',
},
{
id: 'oAuthScope',
label: translations.OAUTH_SCOPE,
isRequired: true,
defaultValue: 'https://securitycenter.onmicrosoft.com/windowsatpservice/.default',
},
{
id: 'apiUrl',
label: translations.API_URL_LABEL,
isUrlField: true,
isRequired: true,
defaultValue: 'https://api.securitycenter.windows.com',
},
];
const secretsFormSchema: SecretsFieldSchema[] = [
{
id: 'clientSecret',
label: translations.CLIENT_SECRET_VALUE_LABEL,
isPasswordField: true,
},
];
const MicrosoftDefenderEndpointActionConnectorFields: React.FunctionComponent<
ActionConnectorFieldsProps
> = ({ readOnly, isEdit }) => (
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={configFormSchema}
secretsFormSchema={secretsFormSchema}
/>
);
// eslint-disable-next-line import/no-default-export
export { MicrosoftDefenderEndpointActionConnectorFields as default };

View file

@ -0,0 +1,39 @@
/*
* 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 { render as reactRender, type RenderResult } from '@testing-library/react';
import MicrosoftDefenderEndpointParamsFields from './microsoft_defender_endpoint_params';
import type { ActionParamsProps } from '@kbn/alerts-ui-shared';
import { MicrosoftDefenderEndpointActionParams } from '../../../common/microsoft_defender_endpoint/types';
import React from 'react';
import { RUN_CONNECTOR_TEST_MESSAGE } from './translations';
describe('Microsoft Defender for Endpoint Params.', () => {
let renderProps: ActionParamsProps<MicrosoftDefenderEndpointActionParams>;
let render: () => RenderResult;
beforeEach(() => {
renderProps = {
errors: {},
editAction: jest.fn(),
actionParams: {},
index: 0,
};
render = () => reactRender(<MicrosoftDefenderEndpointParamsFields {...renderProps} />);
});
it('should render UI with expected message', () => {
const { getByTestId } = render();
expect(getByTestId('msDefenderParams')).toHaveTextContent(RUN_CONNECTOR_TEST_MESSAGE);
});
it('should set subAction to test_connector', () => {
render();
expect(renderProps.editAction).toHaveBeenCalledWith('subAction', 'testConnector', 0);
});
});

View file

@ -0,0 +1,36 @@
/*
* 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 React, { memo, useEffect } from 'react';
import type { ActionParamsProps } from '@kbn/alerts-ui-shared';
import { EuiFormRow, EuiText } from '@elastic/eui';
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '../../../common/microsoft_defender_endpoint/constants';
import { RUN_CONNECTOR_TEST_MESSAGE } from './translations';
import { MicrosoftDefenderEndpointActionParams } from '../../../common/microsoft_defender_endpoint/types';
const MicrosoftDefenderEndpointParamsFields = memo<
ActionParamsProps<MicrosoftDefenderEndpointActionParams>
>(({ editAction, actionParams, index }) => {
useEffect(() => {
if (!actionParams.subAction) {
editAction('subAction', MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR, index);
}
}, [actionParams.subAction, editAction, index]);
return (
<>
<EuiFormRow fullWidth>
<EuiText size="s" data-test-subj="msDefenderParams">
{RUN_CONNECTOR_TEST_MESSAGE}
</EuiText>
</EuiFormRow>
</>
);
});
// eslint-disable-next-line import/no-default-export
export { MicrosoftDefenderEndpointParamsFields as default };

View file

@ -0,0 +1,68 @@
/*
* 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 { i18n } from '@kbn/i18n';
// config form
export const OAUTH_URL_LABEL = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.oAuthUrlLabel',
{
defaultMessage: 'OAuth Server URL',
}
);
export const OAUTH_SCOPE = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.oAuthScope',
{
defaultMessage: 'OAuth scope',
}
);
export const API_URL_LABEL = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.apiUrlLabel',
{
defaultMessage: 'API URL',
}
);
export const CLIENT_ID_LABEL = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.clientIdLabel',
{
defaultMessage: 'Application client ID',
}
);
export const CLIENT_SECRET_VALUE_LABEL = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.clientSecretValueLabel',
{
defaultMessage: 'Client secret value',
}
);
export const TENANT_ID_LABEL = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.config.tenantIdLabel',
{
defaultMessage: 'Tenant ID',
}
);
export const RUN_CONNECTOR_TEST_MESSAGE = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.params.testMessage',
{ defaultMessage: "Run a test to validate the connector's configuration" }
);
export const ACTION_REQUIRED = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.params.error.requiredActionText',
{
defaultMessage: 'Action is required.',
}
);
export const INVALID_ACTION = i18n.translate(
'xpack.stackConnectors.security.MicrosoftDefenderEndpoint.params.error.invalidActionText',
{
defaultMessage: 'Invalid action name.',
}
);

View file

@ -7,6 +7,7 @@
import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
import { getMicrosoftDefenderEndpointConnectorType } from './microsoft_defender_endpoint';
import { getConnectorType as getCasesWebhookConnectorType } from './cases_webhook';
import { getConnectorType as getJiraConnectorType } from './jira';
import { getServiceNowITSMConnectorType } from './servicenow_itsm';
@ -123,4 +124,7 @@ export function registerConnectorTypes({
if (experimentalFeatures.inferenceConnectorOn) {
actions.registerSubActionConnectorType(getInferenceConnectorType());
}
if (experimentalFeatures.microsoftDefenderEndpointOn) {
actions.registerSubActionConnectorType(getMicrosoftDefenderEndpointConnectorType());
}
}

View file

@ -0,0 +1,93 @@
/*
* 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 { AxiosResponse } from 'axios';
import { SubActionConnector } from '@kbn/actions-plugin/server';
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/usage';
/**
* Create an Axios response object mock
*
* @param data
* @param status
* @param statusText
*/
export const createAxiosResponseMock = <R>(
data: R,
status = 200,
statusText = 'ok'
): AxiosResponse<R> => {
return {
data,
status,
statusText,
headers: {},
// @ts-expect-error
config: {},
};
};
export type ConnectorInstanceMock<T extends SubActionConnector<any, any>> = jest.Mocked<
T & {
// Protected methods that will also be exposed for testing purposes
request: SubActionConnector<any, any>['request'];
}
>;
/**
* Creates an instance of the Connector class that is passed in and wraps it in a `Proxy`, and
* intercepts calls to public methods (and a few protected methods) and wraps those in `jest.fn()` so
* that they can be mocked.
*
* For an example on the usage of this factory function, see the Mocks for Microsoft Defender for Endpoint connector.
*
* @param ConnectorClass
* @param constructorArguments
*/
export const createConnectorInstanceMock = <T extends typeof SubActionConnector<any, any>>(
ConnectorClass: T,
constructorArguments: ConstructorParameters<T>[0]
): ConnectorInstanceMock<InstanceType<T>> => {
const requestMock = jest.fn();
const ConnectorClassExtended =
// @ts-expect-error
class extends ConnectorClass {
public async request<R>(
params: SubActionRequestParams<R>,
usageCollector: ConnectorUsageCollector
): Promise<AxiosResponse<R>> {
return requestMock(params, usageCollector);
}
};
// @ts-expect-error
const instance = new ConnectorClassExtended(constructorArguments);
const mockedMethods: { [K in keyof InstanceType<T>]?: jest.Mock } = { request: requestMock };
const instanceAccessorHandler: ProxyHandler<InstanceType<T>> = {};
const proxiedInstance = new Proxy(instance, instanceAccessorHandler) as ConnectorInstanceMock<
InstanceType<T>
>;
instanceAccessorHandler.get = function (target, prop, receiver) {
if (typeof instance[prop] === 'function') {
if (!mockedMethods[prop as keyof InstanceType<T>]) {
mockedMethods[prop as keyof InstanceType<T>] = jest.fn(
instance[prop].bind(proxiedInstance) // << Magic sauce!
);
}
return mockedMethods[prop as keyof InstanceType<T>];
}
// @ts-expect-error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
// eslint-disable-next-line prefer-rest-params
return Reflect.get(...arguments);
};
return proxiedInstance;
};

View file

@ -0,0 +1,62 @@
/*
* 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 {
SubActionConnectorType,
ValidatorType,
} from '@kbn/actions-plugin/server/sub_action_framework/types';
import { EndpointSecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
import { ActionExecutionSourceType, urlAllowListValidator } from '@kbn/actions-plugin/server';
import {
ENDPOINT_SECURITY_EXECUTE_PRIVILEGE,
ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE,
} from '@kbn/actions-plugin/server/feature';
import { MicrosoftDefenderEndpointConnector } from './microsoft_defender_endpoint';
import {
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets,
} from '../../../common/microsoft_defender_endpoint/types';
import {
MicrosoftDefenderEndpointConfigSchema,
MicrosoftDefenderEndpointSecretsSchema,
} from '../../../common/microsoft_defender_endpoint/schema';
import {
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
MICROSOFT_DEFENDER_ENDPOINT_TITLE,
} from '../../../common/microsoft_defender_endpoint/constants';
export const getMicrosoftDefenderEndpointConnectorType = (): SubActionConnectorType<
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets
> => ({
id: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
name: MICROSOFT_DEFENDER_ENDPOINT_TITLE,
getService: (params) => new MicrosoftDefenderEndpointConnector(params),
schema: {
config: MicrosoftDefenderEndpointConfigSchema,
secrets: MicrosoftDefenderEndpointSecretsSchema,
},
validators: [
{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('oAuthServerUrl') },
{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('apiUrl') },
],
supportedFeatureIds: [EndpointSecurityConnectorFeatureId],
minimumLicenseRequired: 'enterprise' as const,
subFeature: 'endpointSecurity',
getKibanaPrivileges: (args) => {
const privileges = [ENDPOINT_SECURITY_EXECUTE_PRIVILEGE];
if (
args?.source === ActionExecutionSourceType.HTTP_REQUEST &&
args?.params?.subAction !== MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR
) {
privileges.push(ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE);
}
return privileges;
},
});

View file

@ -0,0 +1,208 @@
/*
* 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 {
CreateMicrosoftDefenderConnectorMockResponse,
microsoftDefenderEndpointConnectorMocks,
} from './mocks';
describe('Microsoft Defender for Endpoint Connector', () => {
let connectorMock: CreateMicrosoftDefenderConnectorMockResponse;
beforeEach(() => {
connectorMock = microsoftDefenderEndpointConnectorMocks.create();
});
describe('Access Token management', () => {
it('should call API to generate as new token', async () => {
await connectorMock.instanceMock.isolateHost(
{ id: '1-2-3', comment: 'foo' },
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: `${connectorMock.options.config.oAuthServerUrl}/${connectorMock.options.config.tenantId}/oauth2/v2.0/token`,
method: 'POST',
data: {
grant_type: 'client_credentials',
client_id: connectorMock.options.config.clientId,
scope: connectorMock.options.config.oAuthScope,
client_secret: connectorMock.options.secrets.clientSecret,
},
}),
connectorMock.usageCollector
);
});
});
describe('#testConnector', () => {
it('should return expected response', async () => {
Object.entries(connectorMock.apiMock).forEach(([url, responseFn]) => {
connectorMock.apiMock[url.replace('1-2-3', 'elastic-connector-test')] = responseFn;
});
await expect(
connectorMock.instanceMock.testConnector({}, connectorMock.usageCollector)
).resolves.toEqual({
results: [
'API call to Machines API was successful',
'API call to Machine Isolate was successful',
'API call to Machine Release was successful',
'API call to Machine Actions was successful',
],
});
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringMatching(/machines\/elastic-connector-test$/),
}),
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringMatching(/machines\/elastic-connector-test\/isolate$/),
}),
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringMatching(/machines\/elastic-connector-test\/unisolate$/),
}),
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringMatching(/\/machineactions/),
}),
connectorMock.usageCollector
);
});
});
describe('#isolate()', () => {
it('should call isolate api with comment', async () => {
await connectorMock.instanceMock.isolateHost(
{ id: '1-2-3', comment: 'foo' },
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
url: expect.stringMatching(/\/api\/machines\/1-2-3\/isolate$/),
data: { Comment: 'foo', IsolationType: 'Full' },
headers: { Authorization: 'Bearer eyJN_token_JIE' },
}),
connectorMock.usageCollector
);
});
it('should return a Machine Action', async () => {
await expect(
connectorMock.instanceMock.isolateHost(
{ id: '1-2-3', comment: 'foo' },
connectorMock.usageCollector
)
).resolves.toEqual(microsoftDefenderEndpointConnectorMocks.createMachineActionMock());
});
});
describe('#release()', () => {
it('should call isolate api with comment', async () => {
await connectorMock.instanceMock.releaseHost(
{ id: '1-2-3', comment: 'foo' },
connectorMock.usageCollector
);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
url: expect.stringMatching(/\/api\/machines\/1-2-3\/unisolate$/),
data: { Comment: 'foo' },
}),
connectorMock.usageCollector
);
});
it('should return a machine action object', async () => {
await expect(
connectorMock.instanceMock.isolateHost(
{ id: '1-2-3', comment: 'foo' },
connectorMock.usageCollector
)
).resolves.toEqual(microsoftDefenderEndpointConnectorMocks.createMachineActionMock());
});
});
describe('#getActions()', () => {
it('should return expected response', async () => {
await expect(
connectorMock.instanceMock.getActions({}, connectorMock.usageCollector)
).resolves.toEqual({
'@odata.context':
'https://api-us3.securitycenter.microsoft.com/api/$metadata#MachineActions',
'@odata.count': 1,
page: 1,
pageSize: 20,
total: 1,
value: [
{
cancellationComment: '',
cancellationDateTimeUtc: '',
cancellationRequestor: '',
commands: ['RunScript'],
computerDnsName: 'desktop-test',
creationDateTimeUtc: '2019-01-02T14:39:38.2262283Z',
externalID: 'abc',
id: '5382f7ea-7557-4ab7-9782-d50480024a4e',
lastUpdateDateTimeUtc: '2019-01-02T14:40:44.6596267Z',
machineId: '1-2-3',
requestSource: '',
requestor: 'Analyst@TestPrd.onmicrosoft.com',
requestorComment: 'test for docs',
scope: 'Selective',
status: 'Succeeded',
title: '',
type: 'Isolate',
},
],
});
});
it('should call Microsoft API with expected query params', async () => {
await connectorMock.instanceMock.getActions({}, connectorMock.usageCollector);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://api.mock__microsoft.com/api/machineactions',
params: { $count: true, $top: 20 },
}),
connectorMock.usageCollector
);
});
it.each`
title | options | expectedParams
${'single value filters'} | ${{ id: '123', status: 'Succeeded', machineId: 'abc', page: 2 }} | ${{ $count: true, $filter: 'id eq 123 AND status eq Succeeded AND machineId eq abc', $skip: 20, $top: 20 }}
${'multiple value filters'} | ${{ id: ['123', '321'], type: ['Isolate', 'Unisolate'], page: 1, pageSize: 100 }} | ${{ $count: true, $filter: "id in ('123','321') AND type in ('Isolate','Unisolate')", $top: 100 }}
${'page and page size'} | ${{ id: ['123', '321'], type: ['Isolate', 'Unisolate'], page: 3, pageSize: 100 }} | ${{ $count: true, $filter: "id in ('123','321') AND type in ('Isolate','Unisolate')", $skip: 200, $top: 100 }}
`(
'should correctly build the oData URL params: $title',
async ({ options, expectedParams }) => {
await connectorMock.instanceMock.getActions(options, connectorMock.usageCollector);
expect(connectorMock.instanceMock.request).toHaveBeenCalledWith(
expect.objectContaining({
params: expectedParams,
}),
connectorMock.usageCollector
);
}
);
});
});

View file

@ -0,0 +1,313 @@
/*
* 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 { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
import type { AxiosError } from 'axios';
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
import { OAuthTokenManager } from './o_auth_token_manager';
import { MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION } from '../../../common/microsoft_defender_endpoint/constants';
import {
IsolateHostParamsSchema,
ReleaseHostParamsSchema,
TestConnectorParamsSchema,
MicrosoftDefenderEndpointDoNotValidateResponseSchema,
GetActionsParamsSchema,
AgentDetailsParamsSchema,
} from '../../../common/microsoft_defender_endpoint/schema';
import {
MicrosoftDefenderEndpointAgentDetailsParams,
MicrosoftDefenderEndpointIsolateHostParams,
MicrosoftDefenderEndpointBaseApiResponse,
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets,
MicrosoftDefenderEndpointReleaseHostParams,
MicrosoftDefenderEndpointTestConnectorParams,
MicrosoftDefenderEndpointMachine,
MicrosoftDefenderEndpointMachineAction,
MicrosoftDefenderEndpointTestConnector,
MicrosoftDefenderEndpointGetActionsParams,
MicrosoftDefenderEndpointGetActionsResponse,
} from '../../../common/microsoft_defender_endpoint/types';
export class MicrosoftDefenderEndpointConnector extends SubActionConnector<
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets
> {
private readonly oAuthToken: OAuthTokenManager;
private readonly urls: {
machines: string;
machineActions: string;
};
constructor(
params: ServiceParams<MicrosoftDefenderEndpointConfig, MicrosoftDefenderEndpointSecrets>
) {
super(params);
this.oAuthToken = new OAuthTokenManager({
...params,
apiRequest: async (...args) => this.request(...args),
});
this.urls = {
machines: `${this.config.apiUrl}/api/machines`,
// API docs: https://learn.microsoft.com/en-us/defender-endpoint/api/get-machineactions-collection
machineActions: `${this.config.apiUrl}/api/machineactions`,
};
this.registerSubActions();
}
private registerSubActions() {
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_DETAILS,
method: 'getAgentDetails',
schema: AgentDetailsParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST,
method: 'isolateHost',
schema: IsolateHostParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RELEASE_HOST,
method: 'releaseHost',
schema: ReleaseHostParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR,
method: 'testConnector',
schema: TestConnectorParamsSchema,
});
this.registerSubAction({
name: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_ACTIONS,
method: 'getActions',
schema: GetActionsParamsSchema,
});
}
private async fetchFromMicrosoft<R extends MicrosoftDefenderEndpointBaseApiResponse>(
req: Omit<SubActionRequestParams<R>, 'responseSchema'>,
connectorUsageCollector: ConnectorUsageCollector
): Promise<R> {
this.logger.debug(() => `Request:\n${JSON.stringify(req, null, 2)}`);
const bearerAccessToken = await this.oAuthToken.get(connectorUsageCollector);
const response = await this.request<R>(
{
...req,
// We don't validate responses from Microsoft API's because we do not want failures for cases
// where the external system might add/remove/change values in the response that we have no
// control over.
responseSchema:
MicrosoftDefenderEndpointDoNotValidateResponseSchema as unknown as SubActionRequestParams<R>['responseSchema'],
headers: { Authorization: `Bearer ${bearerAccessToken}` },
},
connectorUsageCollector
);
return response.data;
}
protected getResponseErrorMessage(error: AxiosError): string {
const appendResponseBody = (message: string): string => {
const responseBody = JSON.stringify(error.response?.data ?? {});
if (responseBody) {
return `${message}\nURL called: ${error.response?.config?.url}\nResponse body: ${responseBody}`;
}
return message;
};
if (!error.response?.status) {
return appendResponseBody(error.message ?? 'Unknown API Error');
}
if (error.response.status === 401) {
return appendResponseBody('Unauthorized API Error (401)');
}
return appendResponseBody(`API Error: [${error.response?.statusText}] ${error.message}`);
}
private buildODataUrlParams({
filter = {},
page = 1,
pageSize = 20,
}: {
filter: Record<string, string | string[]>;
page: number;
pageSize: number;
}): Partial<BuildODataUrlParamsResponse> {
const oDataQueryOptions: Partial<BuildODataUrlParamsResponse> = {
$count: true,
};
if (pageSize) {
oDataQueryOptions.$top = pageSize;
}
if (page > 1) {
oDataQueryOptions.$skip = page * pageSize - pageSize;
}
const filterEntries = Object.entries(filter);
if (filterEntries.length > 0) {
oDataQueryOptions.$filter = '';
for (const [key, value] of filterEntries) {
const isArrayValue = Array.isArray(value);
if (oDataQueryOptions.$filter) {
oDataQueryOptions.$filter += ' AND ';
}
oDataQueryOptions.$filter += `${key} ${isArrayValue ? 'in' : 'eq'} ${
isArrayValue
? '(' + value.map((valueString) => `'${valueString}'`).join(',') + ')'
: value
}`;
}
}
return oDataQueryOptions;
}
public async testConnector(
_: MicrosoftDefenderEndpointTestConnectorParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointTestConnector> {
const results: string[] = [];
const catchErrorAndIgnoreExpectedErrors = (err: Error) => {
if (err.message.includes('ResourceNotFound')) {
return '';
}
throw err;
};
await this.getAgentDetails({ id: 'elastic-connector-test' }, connectorUsageCollector)
.catch(catchErrorAndIgnoreExpectedErrors)
.then(() => {
results.push('API call to Machines API was successful');
});
await this.isolateHost(
{ id: 'elastic-connector-test', comment: 'connector test' },
connectorUsageCollector
)
.catch(catchErrorAndIgnoreExpectedErrors)
.then(() => {
results.push('API call to Machine Isolate was successful');
});
await this.releaseHost(
{ id: 'elastic-connector-test', comment: 'connector test' },
connectorUsageCollector
)
.catch(catchErrorAndIgnoreExpectedErrors)
.then(() => {
results.push('API call to Machine Release was successful');
});
await this.getActions({ pageSize: 1 }, connectorUsageCollector)
.catch(catchErrorAndIgnoreExpectedErrors)
.then(() => {
results.push('API call to Machine Actions was successful');
});
return { results };
}
public async getAgentDetails(
{ id }: MicrosoftDefenderEndpointAgentDetailsParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointMachine> {
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/machine
return this.fetchFromMicrosoft<MicrosoftDefenderEndpointMachine>(
{ url: `${this.urls.machines}/${id}` },
connectorUsageCollector
);
}
public async isolateHost(
{ id, comment }: MicrosoftDefenderEndpointIsolateHostParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointMachineAction> {
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/isolate-machine
return this.fetchFromMicrosoft<MicrosoftDefenderEndpointMachineAction>(
{
url: `${this.urls.machines}/${id}/isolate`,
method: 'POST',
data: {
Comment: comment,
IsolationType: 'Full',
},
},
connectorUsageCollector
);
}
public async releaseHost(
{ id, comment }: MicrosoftDefenderEndpointReleaseHostParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointMachineAction> {
// API Reference:https://learn.microsoft.com/en-us/defender-endpoint/api/unisolate-machine
return this.fetchFromMicrosoft<MicrosoftDefenderEndpointMachineAction>(
{
url: `${this.urls.machines}/${id}/unisolate`,
method: 'POST',
data: {
Comment: comment,
},
},
connectorUsageCollector
);
}
public async getActions(
{ page = 1, pageSize = 20, ...filter }: MicrosoftDefenderEndpointGetActionsParams,
connectorUsageCollector: ConnectorUsageCollector
): Promise<MicrosoftDefenderEndpointGetActionsResponse> {
// API Reference: https://learn.microsoft.com/en-us/defender-endpoint/api/get-machineactions-collection
// OData usage reference: https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-odata-samples
const response = await this.fetchFromMicrosoft<MicrosoftDefenderEndpointGetActionsResponse>(
{
url: `${this.urls.machineActions}`,
method: 'GET',
params: this.buildODataUrlParams({ filter, page, pageSize }),
},
connectorUsageCollector
);
return {
...response,
page,
pageSize,
total: response['@odata.count'] ?? -1,
};
}
}
interface BuildODataUrlParamsResponse {
$filter: string;
$top: number;
$skip: number;
$count: boolean;
}

View file

@ -0,0 +1,170 @@
/*
* 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 { ServiceParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/usage';
import {
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointMachine,
MicrosoftDefenderEndpointMachineAction,
MicrosoftDefenderEndpointSecrets,
} from '../../../common/microsoft_defender_endpoint/types';
import { MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID } from '../../../common/microsoft_defender_endpoint/constants';
import { MicrosoftDefenderEndpointConnector } from './microsoft_defender_endpoint';
import {
ConnectorInstanceMock,
createAxiosResponseMock,
createConnectorInstanceMock,
} from '../lib/mocks';
export interface CreateMicrosoftDefenderConnectorMockResponse {
options: ServiceParams<MicrosoftDefenderEndpointConfig, MicrosoftDefenderEndpointSecrets>;
apiMock: { [msApiRoute: string]: (...args: any) => any | Promise<any> };
instanceMock: ConnectorInstanceMock<MicrosoftDefenderEndpointConnector>;
usageCollector: ConnectorUsageCollector;
}
const createMicrosoftDefenderConnectorMock = (): CreateMicrosoftDefenderConnectorMockResponse => {
const apiUrl = 'https://api.mock__microsoft.com';
const options: CreateMicrosoftDefenderConnectorMockResponse['options'] = {
configurationUtilities: actionsConfigMock.create(),
connector: { id: '1', type: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID },
config: {
clientId: 'app-1-2-3',
tenantId: 'tenant_elastic',
oAuthServerUrl: 'https://auth.mock__microsoft.com',
oAuthScope: 'https://securitycenter.onmicrosoft.com/windowsatpservice/.default',
apiUrl,
},
secrets: { clientSecret: 'shhhh-secret' },
logger: loggingSystemMock.createLogger(),
services: actionsMock.createServices(),
};
const instanceMock = createConnectorInstanceMock(MicrosoftDefenderEndpointConnector, options);
// Default MS API response mocks. These (or additional ones) can always be defined directly in test
const apiMock: CreateMicrosoftDefenderConnectorMockResponse['apiMock'] = {
[`${options.config.oAuthServerUrl}/${options.config.tenantId}/oauth2/v2.0/token`]: () => {
return createAxiosResponseMock({
token_type: 'Bearer',
expires_in: 3599,
access_token: 'eyJN_token_JIE',
});
},
// Agent Details
[`${apiUrl}/api/machines/1-2-3`]: () => createAxiosResponseMock(createMicrosoftMachineMock()),
// Isolate
[`${apiUrl}/api/machines/1-2-3/isolate`]: () =>
createAxiosResponseMock(createMicrosoftMachineAction()),
// Release
[`${apiUrl}/api/machines/1-2-3/unisolate`]: () =>
createAxiosResponseMock(createMicrosoftMachineAction()),
// Machine Actions
[`${apiUrl}/api/machineactions`]: () =>
createAxiosResponseMock({
'@odata.context':
'https://api-us3.securitycenter.microsoft.com/api/$metadata#MachineActions',
'@odata.count': 1,
value: [createMicrosoftMachineAction()],
}),
};
instanceMock.request.mockImplementation(
async (
...args: Parameters<ConnectorInstanceMock<MicrosoftDefenderEndpointConnector>['request']>
) => {
const url = args[0].url;
if (apiMock[url]) {
return apiMock[url](...args);
}
throw new Error(`API mock for [${url}] not implemented!!`);
}
);
return {
options,
apiMock,
instanceMock,
usageCollector: new ConnectorUsageCollector({
logger: options.logger,
connectorId: 'test-connector-id',
}),
};
};
const createMicrosoftMachineMock = (
overrides: Partial<MicrosoftDefenderEndpointMachine> = {}
): MicrosoftDefenderEndpointMachine => {
return {
id: '1-2-3',
computerDnsName: 'mymachine1.contoso.com',
firstSeen: '2018-08-02T14:55:03.7791856Z',
lastSeen: '2018-08-02T14:55:03.7791856Z',
osPlatform: 'Windows1',
version: '1709',
osProcessor: 'x64',
lastIpAddress: '172.17.230.209',
lastExternalIpAddress: '167.220.196.71',
osBuild: 18209,
healthStatus: 'Active',
rbacGroupId: '140',
rbacGroupName: 'The-A-Team',
riskScore: 'Low',
exposureLevel: 'Medium',
aadDeviceId: '80fe8ff8-2624-418e-9591-41f0491218f9',
machineTags: ['test tag 1', 'test tag 2'],
onboardingstatus: 'foo',
ipAddresses: [
{ ipAddress: '1.1.1.1', macAddress: '23:a2:5t', type: '', operationalStatus: '' },
],
osArchitecture: '',
...overrides,
};
};
const createMicrosoftMachineAction = (
overrides: Partial<MicrosoftDefenderEndpointMachineAction> = {}
): MicrosoftDefenderEndpointMachineAction => {
return {
id: '5382f7ea-7557-4ab7-9782-d50480024a4e',
type: 'Isolate',
scope: 'Selective',
requestor: 'Analyst@TestPrd.onmicrosoft.com',
requestorComment: 'test for docs',
requestSource: '',
status: 'Succeeded',
machineId: '1-2-3',
computerDnsName: 'desktop-test',
creationDateTimeUtc: '2019-01-02T14:39:38.2262283Z',
lastUpdateDateTimeUtc: '2019-01-02T14:40:44.6596267Z',
externalID: 'abc',
commands: ['RunScript'],
cancellationRequestor: '',
cancellationComment: '',
cancellationDateTimeUtc: '',
title: '',
...overrides,
};
};
export const microsoftDefenderEndpointConnectorMocks = Object.freeze({
createAxiosResponseMock,
create: createMicrosoftDefenderConnectorMock,
createMachineMock: createMicrosoftMachineMock,
createMachineActionMock: createMicrosoftMachineAction,
});

View file

@ -0,0 +1,81 @@
/*
* 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 { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/usage';
import { MicrosoftDefenderEndpointDoNotValidateResponseSchema } from '../../../common/microsoft_defender_endpoint/schema';
import {
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets,
MicrosoftDefenderEndpointApiTokenResponse,
} from '../../../common/microsoft_defender_endpoint/types';
export class OAuthTokenManager {
private accessToken: string = '';
private readonly oAuthTokenUrl: string;
constructor(
private readonly params: ServiceParams<
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets
> & {
apiRequest: SubActionConnector<
MicrosoftDefenderEndpointConfig,
MicrosoftDefenderEndpointSecrets
>['request'];
}
) {
const url = new URL(params.config.oAuthServerUrl);
url.pathname = `/${params.config.tenantId}/oauth2/v2.0/token`;
this.oAuthTokenUrl = url.toString();
}
private async generateNewToken(connectorUsageCollector: ConnectorUsageCollector): Promise<void> {
// FYI: API Docs: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#get-a-token
const { oAuthScope, clientId } = this.params.config;
const newToken = await this.params.apiRequest<MicrosoftDefenderEndpointApiTokenResponse>(
{
url: this.oAuthTokenUrl,
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: {
grant_type: 'client_credentials',
client_id: clientId,
scope: oAuthScope,
client_secret: this.params.secrets.clientSecret,
},
responseSchema: MicrosoftDefenderEndpointDoNotValidateResponseSchema,
},
connectorUsageCollector
);
this.params.logger.debug(
() =>
`Successfully created an access token for Microsoft Defend for Endpoint:\n${JSON.stringify({
...newToken.data,
access_token: '[REDACTED]',
})}`
);
this.accessToken = newToken.data.access_token;
}
/**
* Returns the Bearer token that should be used in API calls
*/
public async get(connectorUsageCollector: ConnectorUsageCollector): Promise<string> {
if (!this.accessToken) {
await this.generateNewToken(connectorUsageCollector);
}
if (!this.accessToken) {
throw new Error('Access token for Microsoft Defend for Endpoint not available!');
}
return this.accessToken;
}
}

View file

@ -16,6 +16,7 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import { Readable } from 'stream';
import { createAxiosResponseMock } from '../lib/mocks';
import { SENTINELONE_CONNECTOR_ID } from '../../../common/sentinelone/constants';
import { SentinelOneConnector } from './sentinelone';
import {
@ -154,17 +155,6 @@ const createGetAgentsApiResponseMock = (): SentinelOneGetAgentsResponse => {
};
};
const createAxiosResponseMock = <R>(data: R, status = 200, statusText = 'ok'): AxiosResponse<R> => {
return {
data,
status,
statusText,
headers: {},
// @ts-expect-error
config: {},
};
};
class SentinelOneConnectorTestClass extends SentinelOneConnector {
// Defined details API responses for SentinelOne. These can be manipulated by the tests to mock specific results
public mockResponses = {

View file

@ -44,6 +44,7 @@
"@kbn/alerting-types",
"@kbn/core-notifications-browser",
"@kbn/response-ops-rule-form",
"@kbn/alerts-ui-shared",
],
"exclude": [
"target/**/*",

View file

@ -13,6 +13,7 @@ import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plu
import { ExperimentalConfigKeys } from '@kbn/stack-connectors-plugin/common/experimental_features';
import { SENTINELONE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
import { CROWDSTRIKE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
import { MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
import { services } from './services';
import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers';
@ -55,6 +56,7 @@ const enabledActionTypes = [
'.d3security',
SENTINELONE_CONNECTOR_ID,
CROWDSTRIKE_CONNECTOR_ID,
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
'.slack',
'.slack_api',
'.thehive',

View file

@ -16,5 +16,9 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('./tests')],
useDedicatedTaskRunner: true,
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
experimentalFeatures: [
'sentinelOneConnectorOn',
'crowdstrikeConnectorOn',
'microsoftDefenderEndpointOn',
],
});

View file

@ -16,5 +16,9 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('./tests')],
useDedicatedTaskRunner: false,
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
experimentalFeatures: [
'sentinelOneConnectorOn',
'crowdstrikeConnectorOn',
'microsoftDefenderEndpointOn',
],
});

View file

@ -16,5 +16,9 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('.')],
useDedicatedTaskRunner: true,
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
experimentalFeatures: [
'sentinelOneConnectorOn',
'crowdstrikeConnectorOn',
'microsoftDefenderEndpointOn',
],
});

View file

@ -0,0 +1,260 @@
/*
* 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 { FeaturesPrivileges, Role } from '@kbn/security-plugin-types-common';
import {
MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION,
} from '@kbn/stack-connectors-plugin/common/microsoft_defender_endpoint/constants';
import SuperTest from 'supertest';
import expect from '@kbn/expect';
import { getUrlPrefix } from '../../../../../common/lib';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { createSupertestErrorLogger } from '../../../../../common/lib/log_supertest_errors';
// eslint-disable-next-line import/no-default-export
export default function createMicrosoftDefenderEndpointTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const securityService = getService('security');
const log = getService('log');
const logErrorDetails = createSupertestErrorLogger(log);
interface CreatedUser {
username: string;
password: string;
deleteUser: () => Promise<void>;
}
// TODO:PT create service for user creation since this code is now duplicated across SentinelOne, Crowdstrike and here for MS Defender
const createUser = async ({
username,
password = 'changeme',
kibanaFeatures = { actions: ['all'] },
}: {
username: string;
password?: string;
kibanaFeatures?: FeaturesPrivileges;
}): Promise<CreatedUser> => {
const role: Role = {
name: username,
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
feature: {
// Important: Saved Objects Managemnt should be set to `all` to ensure that authz
// is not defaulted to the check done against SO's for SentinelOne
savedObjectsManagement: ['all'],
...kibanaFeatures,
},
spaces: ['*'],
},
],
};
await securityService.role.create(role.name, {
kibana: role.kibana,
elasticsearch: role.elasticsearch,
});
await securityService.user.create(username, {
password: 'changeme',
full_name: role.name,
roles: [role.name],
});
return {
username,
password,
deleteUser: async () => {
await securityService.user.delete(role.name);
await securityService.role.delete(role.name);
},
};
};
describe('Microsoft Defender for Endpoint Connector', () => {
let connectorId: string = '';
const executeSubAction = async ({
subAction,
subActionParams,
expectedHttpCode = 200,
username = 'elastic',
password = 'changeme',
errorLogger = logErrorDetails,
}: {
supertest: SuperTest.Agent;
subAction: string;
subActionParams: Record<string, unknown>;
expectedHttpCode?: number;
username?: string;
password?: string;
errorLogger?: (err: any) => void;
}) => {
return supertestWithoutAuth
.post(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}/_execute`)
.set('kbn-xsrf', 'foo')
.on('error', errorLogger)
.auth(username, password)
.send({
params: {
subAction,
subActionParams,
},
})
.expect(expectedHttpCode);
};
const subActions: MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION[] = [
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.RELEASE_HOST,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.ISOLATE_HOST,
MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.GET_AGENT_DETAILS,
];
before(async () => {
const response = await supertest
.post(`${getUrlPrefix('default')}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.on('error', logErrorDetails)
.send({
name: 'My sub connector',
connector_type_id: MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID,
config: {
clientId: 'client-abc',
tenantId: 'tenant-123',
oAuthServerUrl: 'https://some.non.existent.com',
oAuthScope: 'scope-a',
apiUrl: 'https://some.non.existent.com',
},
secrets: { clientSecret: 'abc-123' },
})
.expect(200);
connectorId = response.body.id;
});
after(async () => {
if (connectorId) {
await supertest
.delete(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}`)
.set('kbn-xsrf', 'true')
.send()
.expect(({ ok, status }) => {
// Should cover all success codes (ex. 204 (no content), 200, etc...)
if (!ok) {
throw new Error(
`Expected delete to return a status code in the 200, but got ${status}`
);
}
});
connectorId = '';
}
});
describe('Sub-action authz', () => {
describe('and user has NO privileges', () => {
let user: CreatedUser;
before(async () => {
user = await createUser({
username: 'read_access_user',
kibanaFeatures: { actions: ['read'] },
});
});
after(async () => {
if (user) {
await user.deleteUser();
}
});
for (const subActionValue of subActions) {
it(`should deny execute of ${subActionValue}`, async () => {
const execRes = await executeSubAction({
supertest: supertestWithoutAuth,
subAction: subActionValue,
subActionParams: {},
username: user.username,
password: user.password,
expectedHttpCode: 403,
errorLogger: logErrorDetails.ignoreCodes([403]),
});
expect(execRes.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unauthorized to execute a "${MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID}" action`,
});
});
}
});
describe('and user has proper privileges', () => {
let user: CreatedUser;
before(async () => {
user = await createUser({
username: 'all_access_user',
});
});
after(async () => {
if (user) {
await user.deleteUser();
// @ts-expect-error
user = undefined;
}
});
for (const subActionValue of subActions) {
const isAllowedSubAction =
subActionValue === MICROSOFT_DEFENDER_ENDPOINT_SUB_ACTION.TEST_CONNECTOR;
it(`should ${
isAllowedSubAction ? 'allow' : 'deny'
} execute of ${subActionValue}`, async () => {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
body: { status, message, connector_id, statusCode, error },
} = await executeSubAction({
supertest: supertestWithoutAuth,
subAction: subActionValue,
subActionParams: {},
username: user.username,
password: user.password,
...(isAllowedSubAction
? {}
: { expectedHttpCode: 403, errorLogger: logErrorDetails.ignoreCodes([403]) }),
});
if (isAllowedSubAction) {
expect({ status, message, connector_id }).to.eql({
status: 'error',
message: 'an error occurred while running the action',
connector_id: connectorId,
});
} else {
expect({ statusCode, message, error }).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unauthorized to execute a "${MICROSOFT_DEFENDER_ENDPOINT_CONNECTOR_ID}" action`,
});
}
});
}
});
});
});
}

View file

@ -46,6 +46,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
loadTestFile(require.resolve('./connector_types/thehive'));
loadTestFile(require.resolve('./connector_types/bedrock'));
loadTestFile(require.resolve('./connector_types/gemini'));
loadTestFile(require.resolve('./connector_types/microsoft_defender_endpoint'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./execute'));

View file

@ -57,6 +57,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
'.sentinelone',
'.cases',
'.crowdstrike',
'.microsoft_defender_endpoint',
].sort()
);
});

View file

@ -22,5 +22,5 @@ export default createTestConfig('spaces_only', {
testFiles: [require.resolve('.')],
reportName: 'X-Pack Alerting API Integration Tests - Actions',
enableFooterInEmail: false,
experimentalFeatures: ['crowdstrikeConnectorOn'],
experimentalFeatures: ['crowdstrikeConnectorOn', 'microsoftDefenderEndpointOn'],
});

View file

@ -36,7 +36,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--xpack.task_manager.monitored_aggregated_stats_refresh_rate=5000',
'--xpack.task_manager.ephemeral_tasks.enabled=false',
'--xpack.task_manager.ephemeral_tasks.request_capacity=100',
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(['crowdstrikeConnectorOn'])}`,
`--xpack.stack_connectors.enableExperimental=${JSON.stringify([
'crowdstrikeConnectorOn',
'microsoftDefenderEndpointOn',
'inferenceConnectorOn',
])}`,
...findTestPluginPaths(path.resolve(__dirname, 'plugins')),
],
},

View file

@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) {
'actions:.index',
'actions:.inference',
'actions:.jira',
'actions:.microsoft_defender_endpoint',
'actions:.observability-ai-assistant',
'actions:.opsgenie',
'actions:.pagerduty',