mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
cb7cd554ad
commit
91f01011d5
34 changed files with 1957 additions and 16 deletions
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
crowdstrikeConnectorOn: true,
|
||||
inferenceConnectorOn: true,
|
||||
crowdstrikeConnectorRTROn: false,
|
||||
microsoftDefenderEndpointOn: false,
|
||||
});
|
||||
|
||||
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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,
|
||||
]);
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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 };
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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')),
|
||||
};
|
||||
}
|
|
@ -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 };
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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.',
|
||||
}
|
||||
);
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"@kbn/alerting-types",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/response-ops-rule-form",
|
||||
"@kbn/alerts-ui-shared",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
});
|
||||
|
|
|
@ -16,5 +16,9 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('.')],
|
||||
useDedicatedTaskRunner: true,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
|
||||
experimentalFeatures: [
|
||||
'sentinelOneConnectorOn',
|
||||
'crowdstrikeConnectorOn',
|
||||
'microsoftDefenderEndpointOn',
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
|
|||
'.sentinelone',
|
||||
'.cases',
|
||||
'.crowdstrike',
|
||||
'.microsoft_defender_endpoint',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
@ -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')),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue