Microsoft 365 Defender

Microsoft 365 Defender is a unified pre- and post-breach enterprise defense suite that natively coordinates detection, prevention, investigation, and response across endpoints, identities, email, and applications to provide integrated protection against sophisticated attacks.

Network Security · Microsoft 365 Defender

Details

IDMicrosoft 365 Defender
ProviderMicrosoft
CategoryNetwork Security
From Version5.5.0
Docker Imagedemisto/crypto:1.0.0.10120494
Supported ModulesAgentix Cloud Runtime Security XSIAM EDR Cortex Cloud

README

Microsoft 365 Defender is a unified pre- and post-breach enterprise defense suite that natively coordinates detection,
prevention, investigation, and response across endpoints, identities, email, and applications to provide integrated
protection against sophisticated attacks.

Authentication Using the Device Code Flow

Use the device code flow
to link Microsoft 365 Defender with Cortex XSOAR.

To connect to the Microsoft 365 Defender:

  1. Fill in the required parameters.
  2. Run the !microsoft-365-defender-auth-start command.
  3. Follow the instructions that appear.
  4. Run the !microsoft-365-defender-auth-complete command.

At the end of the process you’ll see a message that you’ve logged in successfully.

Note: In case of a password change, the microsoft-365-defender-auth-reset command should be executed followed by the authentication process described above.

Cortex XSOAR App

In order to use the Cortex XSOAR application, use the default application ID.
9093c354-630a-47f1-b087-6768eb9427e6

Self-Deployed Application - Device Code Flow

To use a self-configured Azure application, you need to add a new Azure App Registration in the Azure Portal. For more details, follow Self Deployed Application - Device Code Flow.

Required Permissions

The required API permissions are for the Microsoft Threat Protection app.

  • offline_access - Delegate
  • AdvancedHunting.Read - Delegated
  • Incident.ReadWrite.All - Application - See section 4 in this article
  • AdvancedHunting.Read.All - Application - See section 4 in this article

Self-Deployed Application - Client Credentials Flow

Follow these steps for a self-deployed configuration:

  1. To use a self-configured Azure application, you need to add a new Azure App Registration in the Azure Portal. To add the registration, refer to the following Microsoft article steps 1-8.
  2. In the instance configuration, select the client-credentials checkbox.
  3. Enter your Client/Application ID in the Application ID parameter.
  4. Enter your Client Secret in the Client Secret parameter.
  5. Enter your Tenant ID in the Tenant ID parameter.

Required Permissions

  • AdvancedHunting.Read.All - Application
  • Incident.ReadWrite.All - Application

Configure Microsoft 365 Defender on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Microsoft 365 Defender.
  3. Click Add instance to create and configure a new integration instance.
  4. To ensure that mirroring works:
    1. Select Incident Mirroring Direction. Choose the direction to mirror the incident: Incoming (from Microsoft 365 Defender to Cortex XSOAR), Outgoing (from Cortex XSOAR to Microsoft 365 Defender), or Incoming And Outgoing (from/to Cortex XSOAR and Microsoft 365 Defender).
    2. Select the Fetches incidents radio button.
    3. Under Incident type, select Microsoft 365 Defender Incident.
    4. Under Mapper (incoming), select Microsoft 365 Defender - Incoming Mapper.
    5. Under Mapper (outgoing), select Microsoft 365 Defender - Outgoing Mapper.
    6. To enable mirroring to close a ticket in Cortex XSOAR, check the Close Mirrored Cortex XSOAR Incidents checkbox.
    7. To enable mirroring to close an incident in Microsoft 365 Defender, check the Close Mirrored Microsoft 365 Defender Incidents checkbox.
Parameter Description Required
Endpoint URI The United States: api-us.security.microsoft.com
Europe: api-eu.security.microsoft.com
The United Kingdom: api-uk.security.microsoft.co
True
ID or Client ID   False
Token or Tenant ID   False
Application ID The API key to use to connect. False
Use Client Credentials Authorization Flow Use a self-deployed Azure application and authenticate using the Client Credentials flow. False
Tenant ID (for Client Credentials mode)   False
Client Secret (for Client Credentials mode)   False
Client Secret   False
Certificate Thumbprint Used for certificate authentication. As appears in the “Certificates & secrets” page of the app. False
Private Key   False
Certificate Thumbprint Used for certificate authentication. As appears in the “Certificates & secrets” page of the app. False
Private Key Used for certificate authentication. The private key of the registered certificate. False
Use Azure Managed Identities Relevant only if the integration is running on Azure VM. If selected, authenticates based on the value provided for the Azure Managed Identities Client ID field. If no value is provided for the Azure Managed Identities Client ID field, authenticates based on the System Assigned Managed Identity. For additional information, see the Help tab. False
Azure Managed Identities Client ID The Managed Identities client ID for authentication - relevant only if the integration is running on Azure VM. False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)   False
Fetch incidents timeout The time limit in seconds for fetch incidents to run. Leave this empty to cancel the timeout limit. False
Number of incidents for each fetch. Due to API limitations, the maximum is 100. False
Incident type   False
Fetch incidents   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Application ID (Deprecated)   False
Tenant ID (for Client Credentials mode) (Deprecated)   False
Client Secret (for Client Credentials mode) (Deprecated)   False
Incidents Fetch Interval   False
Incident Mirroring Direction Choose the direction to mirror the incident: Incoming (from Microsoft 365 Defender to Cortex XSOAR), Outgoing (from Cortex XSOAR to Microsoft 365 Defender), or Incoming and Outgoing (from/to Cortex XSOAR and Microsoft 365 Defender). False
Close Mirrored Cortex XSOAR Incidents Incoming Mirroring - when selected, closing the Microsoft 365 Defender incident is mirrored in Cortex XSOAR. False
Close Mirrored Microsoft 365 Defender Incidents Outgoing Mirroring - when selected, closing the Cortex XSOAR incident is mirrored in Microsoft 365 Defender. False
Comment Entry Tag To Microsoft 365 Defender Choose a tag to add to an entry to mirror it as a comment into Microsoft 365 Defender. False
Comment Entry Tag From Microsoft 365 Defender Choose a tag to add to an entry to mirror it as a comment from Microsoft 365 Defender. False
Custom close-reason mapping (XSOAR -> Defender mirrored incident. Overwrites default close-reason mapping defined by Cortex XSOAR) Define how to close the mirrored incidents from Cortex XSOAR into Defender with a custom close reason mapping. Enter a comma-separated list of close reasons (acceptable format {Cortex XSOAR close reason}={Defender close reason}) to override the default close reason mapping defined by Cortex XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and Defender. Not following this format will result in closing the incident with a default close reason. Example: “Resolved=Other,Duplicate=Other”. Refer to Outgoing Closing Logic (XSOAR → Microsoft 365 Defender) for possible close-reasons. False
Custom close-reason mapping (Defender -> XSOAR mirrored incident. Overwrites default close-reason mapping defined by Cortex XSOAR) Define how to close the mirrored incidents from Defender into Cortex XSOAR with a custom close reason mapping. Enter a comma-separated list of close reasons (acceptable format {Defender close reason}={Cortex XSOAR close reason}) to override the default close reason mapping defined by Cortex XSOAR. Note that the mapping must be configured accordingly with the existing close reasons in Cortex XSOAR and Defender. Not following this format will result in closing the incident with a default close reason. Example: “Known Issue=Resolved, Duplicate Incident=Other”. Refer to Incoming Closing Logic (Microsoft 365 Defender → XSOAR) for possible close-reasons. False
  1. Run the !microsoft-365-defender-auth-test command to validate the authentication process.

Incident Mirroring

Mirroring In (Microsoft 365 Defender → XSOAR)

When incidents are mirrored into XSOAR from Microsoft 365 Defender:

  1. Comments mirrored from Microsoft 365 Defender will be added to the incident as entries in XSOAR and tagged with the Comment Entry Tag From Microsoft 365 Defender.
    • By default, the tag is set to: CommentFromMicrosoft365Defender.
  2. If an incident is closed in Microsoft 365 Defender (status = Resolved) and Close Mirrored Cortex XSOAR Incidents is enabled:
    • The “classification” field in Microsoft 365 Defender will be mapped to the Close Reason field in XSOAR.

Mirroring Out (XSOAR → Microsoft 365 Defender)

When incidents are mirrored out from XSOAR to Microsoft 365 Defender:

  1. Supported Fields:
    The following fields are mirrored:
    • Microsoft 365 Defender Status
    • Assigned User
    • Microsoft 365 Defender Classification
    • Microsoft 365 Defender Tags
  2. Comments:
    • XSOAR entries with the Comment Entry Tag To Microsoft 365 Defender tag are mirrored as comments in Microsoft 365 Defender.
    • Note: Comments cannot be edited or deleted in Microsoft 365 Defender, as this functionality is not supported.
  3. If an incident is closed in XSOAR and Close Mirrored Microsoft 365 Defender Incidents is enabled:
    • The Close Reason field is mirrored to the status, classification and determination fields in Microsoft 365 Defender.

Closing Logic

Incoming Closing Logic (Microsoft 365 Defender → XSOAR)

When an incident is resolved in Microsoft 365 Defender:

  1. If the status = Resolved:
    • The “classification” field will be mapped to the Close Reason field in XSOAR using the Close Reason Mapping Table.
    • The “classification” and “determination” fields will be mirrored into the Microsoft 365 Defender Classification field in XSOAR.

Example:

  • An incident closed in Microsoft 365 Defender with:
    • status = Resolved
    • Classification = True Positive
    • Determination = Phishing

    Results in XSOAR:

    • Close Reason = Resolved
    • Microsoft 365 Defender Classification = True Positive - Phishing

Close Reason Mapping Table (Incoming):

Classification in Microsoft 365 Defender Close Reason in XSOAR
Not set Other
True Positive Resolved
False Positive False Positive
Informational / Expected Activity Resolved

Outgoing Closing Logic (XSOAR → Microsoft 365 Defender)

Disclaimer: The closing form currently does not support the Microsoft 365 Defender Classification field, and False Positive Classification is not yet enforced when selecting “False Positive” as the close reason.
If classification is required, please manually update the Microsoft 365 Defender Classification via the layout before closing the incident.

When closing incidents from XSOAR to Microsoft 365 Defender, the following logic is applied to map the Close Reason field:

  1. General Status Update:
    When an incident is marked as closed in XSOAR, the status field in Microsoft 365 Defender will always be set to Resolved.

  2. Close Reason: Resolved or False Positive
    If the Close Reason in XSOAR is Resolved, the Microsoft 365 Defender Classification field is mirrored to classification and determination fields as-is into Microsoft 365 Defender without any changes.

  3. Close Reason: Other or Duplicate
    If the Close Reason in XSOAR is either Other or Duplicate, the following updates occur:

    • classificationUnknown
    • determinationNotAvailable

Close Reason Mapping Table (Outgoing):

Close Reason in XSOAR Classification in Microsoft 365 Defender Determination in Microsoft 365 Defender Status in Defender
Resolved Mirrored as-is Mirrored as-is Resolved
FalsePositive FalsePositive Mirrored as-is \ Other Resolved
Other Unknown NotAvailable Resolved
Duplicate Unknown NotAvailable Resolved

Notes

  1. Configuration Requirements for Closing Incidents:
    To close incidents in both directions, ensure the following settings in XSOAR:
    • Close Mirrored XSOAR Incident checkbox = True
    • Close Mirrored Microsoft 365 Defender Incident checkbox = True

    If these checkboxes are False, only the Microsoft 365 Defender Status and Microsoft 365 Defender Classification fields will be updated, and incidents will not be closed.

Configure Incident Mirroring

This feature is compliant with XSOAR version 6.0 and above.
When mirroring incidents, you can make changes in Microsoft 365 Defender that will be reflected in Cortex XSOAR, or vice versa.

The following instructions include steps for configuring the integration and incoming and outgoing mappers. However, they do not cover every option available in the integration nor classification and mapping features.
For information about classification and mapping see Classification and Mapping.

Note:

  • For Cortex XSOAR version 6.1 only, the final source of truth for an incident are the values in Cortex XSOAR. For example, if you change the severity in Cortex XSOAR and then change it back in Microsoft 365 Defender, the final value that will be presented is the one in Cortex XSOAR. For versions 6.2 and later, if mirroring is in both directions then the latest update is the source of truth.
  • The mirroring settings apply only for incidents that are fetched after applying the settings.
  • To use a custom mapper, you must first duplicate the mapper and edit the field in the copy of the mapper. If you detach the out of the box mapper and make changes to it, the pack does not automatically get updates.

STEP 1 - Configure the Microsoft 365 Defender Integration Instance for Mirroring

  1. Navigate to Integrations and search for Microsoft 365 Defender.
  2. Click Add instance.
  3. Select Fetches incidents.
  4. Select the Incident Mirroring Direction:
    • Incoming - Mirrors changes on the Microsoft 365 Defender incident in to the Cortex XSOAR incident.
    • Outgoing - Mirrors changes on the Cortex XSOAR incident to the Microsoft 365 Defender incident.
    • Incoming And Outgoing - Mirrors changes both in and out on both incidents.
  5. Under Incident type, select Microsoft 365 Defender Incident.
  6. Under Mapper (incoming), for default mapping select Microsoft 365 Defender - Incoming Mapper. For custom mapping, follow the instructions in STEP 2 and then select the custom mapper name.
  7. Under Mapper (outgoing), for default mapping select Microsoft 365 Defender - Outgoing Mapper. For custom mapping, follow the instructions in STEP 3 and then select the custom mapper name.
  8. Enter the relevant Comment Entry Tag To Microsoft 365 Defender and Comment Entry Tag From Microsoft 365 Defender values.
    These values are mapped to the dbotMirrorTags incident field in Cortex XSOAR, which defines how Cortex XSOAR handles comments when you tag them in the War Room.
    Note:
    These tags work only for mirroring comments from Cortex XSOAR to Microsoft 365 Defender.
  9. To enable mirroring when closing an incident in Cortex XSOAR and Microsoft 365 Defender, select the Close Mirrored Cortex XSOAR Incidents and Close Mirrored Microsoft 365 Defender Incidents checkboxes respectively.
  10. Click Save & Exit.

STEP 2 (Optional) Configure the Incoming Mapper by Incident Type

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping and for Incidents search for the Microsoft 365 Defender - Incoming Mapper.
  2. Select it and click Duplicate.
  3. Under the Incident Type dropdown, select Microsoft 365 Defender Incident.
  4. Verify the mapper has these fields mapped. They will pull the values configured on the integration instance settings at the time of ingestion.
    • dbotMirrorId - dbotMirrorId - the field used by the third-party integration to identify the incident. This should be the incidentId of the Microsoft 365 Defender Incident.
    • dbotMirrorDirection - determines whether mirroring is incoming, outgoing, or both. Default is Both. This should match the instance configuration.
    • dbotMirrorInstance - determines the Microsoft 365 Defender instance with which to mirror. This should match the instance configuration.
    • dbotMirrorLastSync - determines the field by which to indicate the last time that the systems synchronized.
    • dbotMirrorTags - determines the tags that you need to add in Cortex XSOAR for entries to be pushed to Microsoft 365 Defender. They should be copied from the tags in the instance configuration. These are also the tags that must be put on the War Room record in order for it to sync.

STEP 3 - Modify the Outgoing Mapper

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping, and for Incidents search for the Microsoft 365 Defender - Outgoing Mapper.
  2. Select it and click Duplicate.
    The left side of the screen shows the Microsoft 365 Defender fields to which to map and the right side of the
    screen shows the Cortex XSOAR fields by which you are mapping.
  3. Under the Incident Type dropdown, select the relevant incident type (for example Microsoft 365 Defender Incident).
  4. Under Schema Type, select incident. The Schema Type represents the Microsoft 365 Defender entity that
    you are mapping to.
  5. On the right side of the screen, under Incident, select the incident based on which you want to
    match.
  6. Change the mapping according to your needs, including any fields you want mapped outward to Microsoft 365 Defender.
  7. Save your changes.

STEP 4 - Create an Incident in Microsoft 365 Defender

For purposes of this use case, it can be a simple incident. The new incident will be ingested in Cortex XSOAR in approximately one minute.

STEP 5 - Add a Comment from Cortex XSOAR to Microsoft 365 Defender

In the example below, we have written A comment from Cortex XSOAR to Microsoft 365 Defender.

  1. Create an entry in the incidents’ war room.
  2. Click Actions > Tags and add the Comment Entry Tag To Microsoft 365 Defender tag.
  3. Navigate back to the incident in Microsoft 365 Defender and within approximately one minute, the changes will be reflected there, too. The note is mirrored out as a comment in Microsoft 365 Defender.
    You can make additional changes like closing the incident or changing the assignee and those will be reflected in both systems.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. After you
successfully execute a command, a DBot message appears in the War Room with the command details.

microsoft-365-defender-auth-start


Run this command to start the authorization process and follow the instructions in the command results. (for device-code mode)

Base Command

microsoft-365-defender-auth-start

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-365-defender-auth-start

Human Readable Output

###Authorization instructions

  1. To sign in, use a web browser to open the page {URL}
    and enter the code {code} to authenticate.
  2. Run the !microsoft-365-defender-auth-complete command in the War Room.

microsoft-365-defender-auth-complete


Run this command to complete the authorization process. Should be used after running the microsoft-365-defender-auth-start command. (for device-code mode)

Base Command

microsoft-365-defender-auth-complete

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-365-defender-auth-complete

Human Readable Output

✅ Authorization completed successfully.

microsoft-365-defender-auth-reset


Run this command if for some reason you need to rerun the authentication process.

Base Command

microsoft-365-defender-auth-reset

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-365-defender-auth-reset

Human Readable Output

Authorization was reset successfully.
You can now run !microsoft-365-defender-auth-start and
!microsoft-365-defender-auth-complete.

microsoft-365-defender-auth-test


Tests the connectivity to the Microsoft 365 Defender.

Base Command

microsoft-365-defender-auth-test

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-365-defender-auth-test

Human Readable Output

✅ Success!

microsoft-365-defender-incidents-list


Get the most recent incidents.

Base Command

microsoft-365-defender-incidents-list

Input

Argument Name Description Required
status Categorize incidents (as Active, Resolved, or Redirected). Possible values are: Active, Resolved, Redirected. Optional
assigned_to Owner of the incident. Optional
limit Number of incidents in the list. Maximum is 100. Default is 100. Optional
offset Number of entries to skip. Optional
timeout The time limit in seconds for the http request to run. Default is 30. Optional
odata Filter incidents using odata query: https://docs.microsoft.com/en-us/microsoft-365/security/defender/api-list-incidents?view=o365-worldwide. Example: {"$filter":"lastUpdateTime gt 2022-08-29T06:00:00.29Z"}. Optional

Context Output

Path Type Description
Microsoft365Defender.Incident.incidentId Number Incident’s ID.
Microsoft365Defender.Incident.redirectIncidentId Unknown Only populated in case an incident is grouped together with another incident, as part of the incident processing logic.
Microsoft365Defender.Incident.incidentName String The name of the incident.
Microsoft365Defender.Incident.createdTime Date The date and time (in UTC) the incident was created.
Microsoft365Defender.Incident.lastUpdateTime Date The date and time (in UTC) the incident was last updated.
Microsoft365Defender.Incident.assignedTo String Owner of the incident.
Microsoft365Defender.Incident.classification String Specification of the incident. Possible values are: Unknown, FalsePositive, and TruePositive.
Microsoft365Defender.Incident.determination String The determination of the incident. Possible values are: NotAvailable, Apt, Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, and Other.
Microsoft365Defender.Incident.status String The current status of the incident. Possible values are: Active, Resolved, and Redirected.
Microsoft365Defender.Incident.severity String Severity of the incident. Possible values are: UnSpecified, Informational, Low, Medium, and High.
Microsoft365Defender.Incident.alerts Unknown List of alerts relevant for the incidents.
Microsoft365Defender.Incident.tags unknown List of custom tags associated with an incident, for example to flag a group of incidents with a common characteristic.
Microsoft365Defender.Incident.comments unknown List of comments created by secops when managing the incident, for example additional information about the classification selection.

Command Example

!ms-365-defender-incidents-list status=Active limit=10 assigned_to=user

Human Readable Output

Incidents

Incident name Tags Severity Incident ID Categories Impacted entities Active alerts Service sources Detection sources First activity Last activity Status Assigned to Classification Device groups
Automated investigation started manually on one endpoint tag1, tag2 Informational 263 SuspiciousActivity user 5 / 12 MicrosoftDefenderForEndpoint AutomatedInvestigation 2021-03-22T12:34:31.8123759Z 2021-03-22T12:59:07.526847Z Active email Unknown computer
Impossible travel activity involving one user   Medium 264 InitialAccess user 1 / 1 MicrosoftCloudAppSecurity MCAS 2021-04-05T06:56:06.833Z 2021-04-05T15:34:25.736Z Resolved email Unknown  

microsoft-365-defender-incident-get


Get incident with the given ID.

Base Command

microsoft-365-defender-incident-get

Input

microsoft-365-defender-incident-get


Gets the incident with the given ID.

Base Command

microsoft-365-defender-incident-get

Input

Argument Name Description Required
id Incident’s ID. Required
timeout The time limit in seconds for the http request to run. Default is 30. Optional

Context Output

Path Type Description
Microsoft365Defender.Incident.incidentId number Incident’s ID.
Microsoft365Defender.Incident.redirectIncidentId unknown Only populated in case an incident is grouped together with another incident, as part of the incident processing logic.
Microsoft365Defender.Incident.incidentName string The name of the incident.
Microsoft365Defender.Incident.createdTime date The date and time (in UTC) the incident was created.
Microsoft365Defender.Incident.tags unknown List of custom tags associated with an incident, for example to flag a group of incidents with a common characteristic.
Microsoft365Defender.Incident.lastUpdateTime date The date and time (in UTC) the incident was last updated.
Microsoft365Defender.Incident.assignedTo string Owner of the incident.
Microsoft365Defender.Incident.classification string Specification of the incident. Possible values are: Unknown, FalsePositive, and TruePositive.
Microsoft365Defender.Incident.determination string The determination of the incident. Possible values are: NotAvailable, Apt, Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, and Other.
Microsoft365Defender.Incident.severity string Severity of the incident. Possible values are: UnSpecified, Informational, Low, Medium, and High.
Microsoft365Defender.Incident.status string The current status of the incident. Possible values are: Active, Resolved, and Redirected.
Microsoft365Defender.Incident.alerts unknown List of alerts relevant for the incidents.
Microsoft365Defender.Incident.tags unknown List of custom tags associated with an incident, for example to flag a group of incidents with a common characteristic.
Microsoft365Defender.Incident.comments unknown List of comments created by secops when managing the incident, for example additional information about the classification selection.

microsoft-365-defender-incident-update


Update the incident with the given ID.

Base Command

microsoft-365-defender-incident-update

Input

Argument Name Description Required
status Categorize incidents (as Active, Resolved, or Redirected). Possible values are: Active, Resolved, Redirected, InProgress. Optional
assigned_to Owner of the incident. Optional
id Incident’s ID. Required
classification The specification for the incident. Possible values are: Unknown, FalsePositive, TruePositive, InformationalExpectedActivity. Optional
determination Determination of the incident. Must be used with the classification field. Possible values depend on the classification field: TruePositive - MultiStagedAttack, MaliciousUserActivity, Malware, Phishing, CompromisedAccount, UnwantedSoftware, Other (default), InformationalExpectedActivity- SecurityTesting, LineOfBusinessApplication, ConfirmedActivity, Other (default), FalsePositive - NotMalicious, NoEnoughDataToValidate, Other (default), Unknown - NotAvailable. Possible values are: NotAvailable, Malware, SecurityTesting, UnwantedSoftware, MultiStagedAttack, MaliciousUserActivity, CompromisedAccount, Phishing, LineOfBusinessApplication, ConfirmedActivity, NotMalicious, Other. Optional
comment Comment to be added to the incident. Optional
tags A comma-separated list of custom tags associated with an incident. For example: tag1,tag2,tag3. Optional
timeout The time limit in seconds for the http request to run. Default is 30. Optional

Context Output

Path Type Description
Microsoft365Defender.Incident.incidentId Number Incident’s ID.
Microsoft365Defender.Incident.redirectIncidentId Unknown Only populated in case an incident is grouped together with another incident, as part of the incident processing logic.
Microsoft365Defender.Incident.incidentName String The name of the incident.
Microsoft365Defender.Incident.createdTime Date The date and time (in UTC) the incident was created.
Microsoft365Defender.Incident.lastUpdateTime Date The date and time (in UTC) the incident was last updated.
Microsoft365Defender.Incident.assignedTo String Owner of the incident.
Microsoft365Defender.Incident.classification String Specification of the incident. Possible values are: Unknown, FalsePositive, and TruePositive.
Microsoft365Defender.Incident.determination String The determination of the incident. Possible values are: NotAvailable, Apt, Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, and Other.
Microsoft365Defender.Incident.severity String Severity of the incident. Possible values are: UnSpecified, Informational, Low, Medium, and High.
Microsoft365Defender.Incident.status String The current status of the incident. Possible values are: Active, Resolved, and Redirected.
Microsoft365Defender.Incident.alerts Unknown List of alerts relevant for the incidents.
Microsoft365Defender.Incident.tags unknown List of custom tags associated with an incident, for example to flag a group of incidents with a common characteristic.
Microsoft365Defender.Incident.comments unknown List of comments created by secops when managing the incident, for example additional information about the classification selection.

Command Example

!microsoft-365-defender-incident-update id=264 tags=test5

Human Readable Output

Updated incident No. 263

Incident name Tags Severity Incident ID Categories Impacted entities Active alerts Service sources Detection sources First activity Last activity Status Assigned to Classification Device groups
Automated investigation started manually on one endpoint test5 Informational 263 SuspiciousActivity   10 / 12 MicrosoftDefenderForEndpoint AutomatedInvestigation 2021-03-22T12:34:31.8123759Z 2021-03-22T12:59:07.526847Z Active User Unknown computer

microsoft-365-defender-advanced-hunting

NOTE: This endpoint will be deprecated by Microsoft on January 31, 2027. We recommend switching to the Microsoft Graph Security Integration and using the msg-advanced-hunting command. For more information, see the Microsoft documentation.


Advanced hunting is a threat-hunting tool that uses specially constructed queries to examine the past 30 days of event data in Microsoft 365 Defender.
Details on how to write queries you can find here.

Base Command

microsoft-365-defender-advanced-hunting

Input

Argument Name Description Required
query Advanced hunting query. Required
timeout The time limit in seconds for the http request to run. Default is 30. Optional
limit Number of entries. Enter -1 for unlimited query. Default is 50. Required

Context Output

Path Type Description
Microsoft365Defender.Hunt.query String The query used, also acted as a key.
Microsoft365Defender.Hunt.results. Unknown The results of the query.

Command Example

!microsoft-365-defender-advanced-hunting query=AlertInfo

Human Readable Output

Result of query: AlertInfo

Timestamp AlertId Title Category Severity ServiceSource DetectionSource AttackTechniques
2021-04-25T10:11:00Z alertId eDiscovery search started or exported InitialAccess Medium Microsoft Defender for Office 365 Microsoft Defender for Office 365  

get-mapping-fields


Returns the list of fields to map in outgoing mirroring. This command is only used for debugging purposes.

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

update-remote-system


Updates the remote incident with local incident changes. This method is only used for debugging purposes and will not update the current incident.

Base Command

update-remote-system

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

get-remote-data


Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.

Base Command

get-remote-data

Input

Argument Name Description Required
id The remote incident ID. Required
lastUpdate The UTC timestamp in seconds of the last update. The incident is only updated if it was modified after the last update time. Default is 0. Optional

Context Output

There is no context output for this command.

get-modified-remote-data


Get the list of incidents that were modified since the last update time. This method is used for debugging purposes. The get-modified-remote-data command is used as part of the Mirroring feature that was introduced in Cortex XSOAR version 6.1.

Base Command

get-modified-remote-data

Input

Argument Name Description Required
lastUpdate Date string representing the local time. The incident is only returned if it was modified after the last update time. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • base_url — Endpoint URI (required)
  • creds_client_id
  • creds_tenant_id
  • _app_id — Application ID
  • client_credentials — Use Client Credentials Authorization Flow
  • _tenant_id — Tenant ID (for Client Credentials mode)
  • credentials — Client Secret (for Client Credentials mode)
  • creds_certificate — Certificate Thumbprint
  • certificate_thumbprint — Certificate Thumbprint
  • private_key — Private Key
  • use_managed_identities — Use Azure Managed Identities
  • managed_identities_client_id
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • fetch_timeout — Fetch incidents timeout
  • max_fetch — Number of incidents for each fetch.
  • incidentType — Incident type
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • app_id — Application ID (Deprecated)
  • tenant_id — Tenant ID (for Client Credentials mode) (Deprecated)
  • enc_key — Client Secret (for Client Credentials mode) (Deprecated)
  • incidentFetchInterval — Incidents Fetch Interval
  • mirror_direction — Incident Mirroring Direction
  • close_incident — Close Mirrored Cortex XSOAR Incidents
  • custom_xsoar_to_defender_close_reason — Enable custom XSOAR to Defender Close Reason
  • custom_defender_to_xsoar_close_reason — Enable custom Defender to XSOAR Close Reason
  • close_out — Close Mirrored Microsoft 365 Defender Incidents
  • comment_tag — Comment Entry Tag To Microsoft 365 Defender
  • comment_tag_from_microsoft365defender — Comment Entry Tag From Microsoft 365 Defender
  • custom_xsoar_to_defender_close_reason_mapping — Custom close-reason mapping (XSOAR -> Defender mirrored incident)
  • custom_defender_to_xsoar_close_reason_mapping — Custom close-reason mapping (Defender -> XSOAR mirrored incident)

Commands (12)

  • get-mapping-fields

    Returns the list of fields to map in outgoing mirroring. This command is only used for debugging purposes.

  • get-modified-remote-data

    Get the list of incidents that were modified since the last update time. This method is used for debugging purposes. The get-modified-remote-data command is used as part of the Mirroring feature that was introduced in Cortex XSOAR version 6.1.

  • get-remote-data

    Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes only.

  • microsoft-365-defender-advanced-hunting

    Advanced hunting is a threat-hunting tool that uses specially constructed queries to examine the past 30 days of event data in Microsoft 365 Defender. Details on how to write queries: https://docs.microsoft.com/en-us/microsoft-365/security/defender/advanced-hunting-query-language?view=o365-worldwide.

  • microsoft-365-defender-auth-complete

    Run this command to complete the authorization process. Should be used after running the microsoft-365-defender-auth-start command. (for device-code mode).

  • microsoft-365-defender-auth-reset

    Run this command if for some reason you need to rerun the authentication process.

  • microsoft-365-defender-auth-start

    Run this command to start the authorization process and follow the instructions in the command results. (for device-code mode).

  • microsoft-365-defender-auth-test

    Tests the connectivity to the Microsoft 365 Defender.

  • microsoft-365-defender-incident-get

    Gets the incident with the given ID.

  • microsoft-365-defender-incident-update

    Update the incident with the given ID.

  • microsoft-365-defender-incidents-list

    Get the most recent incidents.

  • update-remote-system

    Updates the remote incident with local incident changes. This method is only used for debugging purposes and will not update the current incident.

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401
from MicrosoftApiModule import *  # noqa: E402

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

MAX_ENTRIES = 100
TIMEOUT = "30"
TIMEOUT_INT = 30
BASE_URL = "https://api.security.microsoft.com"

MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}

OUTGOING_MIRRORED_FIELDS = {
    "status": "Specifies the current status of the incident.",
    "assignedTo": "Owner of the incident.",
    "classification": "Specification of the incident.",
    "determination": "Specifies the determination of the incident.",
    "tags": "List of Incident tags.",
    "comment": "Comment to be added to the incident.",
}

CLASSIFICATION_DETERMINATION_MAPPING = {
    "TruePositive": [
        "MultiStagedAttack",
        "MaliciousUserActivity",
        "Malware",
        "Phishing",
        "CompromisedAccount",
        "UnwantedSoftware",
        "Other",
    ],
    "InformationalExpectedActivity": ["SecurityTesting", "LineOfBusinessApplication", "ConfirmedActivity", "Other"],
    "FalsePositive": ["NotMalicious", "NoEnoughDataToValidate", "Other"],
    "Unknown": ["NotAvailable"],
}

MICROSOFT_RESOLVED_CLASSIFICATION_TO_XSOAR_CLOSE_REASON = {
    "Unknown": "Other",
    "TruePositive": "Resolved",
    "FalsePositive": "False Positive",
    "InformationalExpectedActivity": "Resolved",
}

MICROSOFT_INCIDENT_ID_KEY = "incidentId"
MIRRORED_OUT_XSOAR_ENTRY_TO_MICROSOFT_COMMENT_INDICATOR = "Mirrored from Cortex XSOAR"
COMMENT_TO_MICROSOFT_DEFAULT_TAG = "CommentToMicrosoft365Defender"

""" CLIENT CLASS """


class Client:
    @logger
    def __init__(
        self,
        app_id: str,
        verify: bool,
        proxy: bool,
        base_url: str = BASE_URL,
        tenant_id: str = None,
        enc_key: str = None,
        client_credentials: bool = False,
        certificate_thumbprint: Optional[str] = None,
        private_key: Optional[str] = None,
        managed_identities_client_id: Optional[str] = None,
    ):
        if app_id and "@" in app_id:
            app_id, refresh_token = app_id.split("@")
            integration_context = get_integration_context()
            integration_context.update(current_refresh_token=refresh_token)
            set_integration_context(integration_context)

        self.client_credentials = client_credentials
        client_args = assign_params(
            base_url=base_url,
            verify=verify,
            proxy=proxy,
            ok_codes=(200, 201, 202, 204),
            scope="offline_access https://security.microsoft.com/mtp/.default",
            self_deployed=True,  # We always set the self_deployed key as True because when not using a self
            # deployed machine, the DEVICE_CODE flow should behave somewhat like a self deployed
            # flow and most of the same arguments should be set, as we're !not! using OProxy.
            auth_id=app_id,
            grant_type=CLIENT_CREDENTIALS if client_credentials else DEVICE_CODE,
            # used for device code flow
            resource="https://api.security.microsoft.com" if not client_credentials else None,
            token_retrieval_url="https://login.windows.net/organizations/oauth2/v2.0/token" if not client_credentials else None,
            # used for client credentials flow
            tenant_id=tenant_id,
            enc_key=enc_key,
            certificate_thumbprint=certificate_thumbprint,
            private_key=private_key,
            managed_identities_client_id=managed_identities_client_id,
            managed_identities_resource_uri=Resources.security,
            command_prefix="microsoft-365-defender",
        )
        self.ms_client = MicrosoftClient(**client_args)  # type: ignore

    @logger
    def incidents_list(
        self,
        timeout: int,
        limit: int = MAX_ENTRIES,
        status: Optional[str] = None,
        assigned_to: Optional[str] = None,
        from_date: Optional[datetime] = None,
        skip: Optional[int] = None,
        odata: Optional[dict] = None,
        last_update_time: Optional[str] = None,
    ) -> dict:
        """
        GET request from the client using OData operators:
            - $top: how many incidents to receive, maximum value is 100
            - $filter: OData query to filter the the list on the properties:
                                                                lastUpdateTime, createdTime, status, and assignedTo
        Args:
            limit (int): how many incidents to receive, the maximum value is 100
            status (str): filter list to contain only incidents with the given status (Active, Resolved, or Redirected)
            assigned_to (str): Owner of the incident, or None if no owner is assigned
            timeout (int): The amount of time (in seconds) that a request will wait for a client to
                establish a connection to a remote machine before a timeout occurs.
            from_date (datetime): get incident with creation date more recent than from_date
            skip (int): how many entries to skip
            odata (dict): alternative method for listing incidents, accepts dictionary of URI parameters
            last_update_time (str): get incident with last update time more recent than last_update_time.

        Returns (Dict): request results as dict:
                    { '@odata.context',
                      'value': list of incidents,
                      '@odata.nextLink'
                    }

        """
        params = {}

        if odata:
            params = odata
        else:
            filter_query = ""
            params = {"$top": limit}
            if status:
                filter_query += "status eq " + "'" + status + "'"

            if assigned_to:
                filter_query += " and " if filter_query else ""
                filter_query += f"assignedTo eq '{assigned_to}'"

            # fetch incidents
            if from_date:
                filter_query += " and " if filter_query else ""
                filter_query += f"createdTime gt {from_date}"

            # mirroring get-modified-remote-data
            if last_update_time:
                filter_query += " and " if filter_query else ""
                filter_query += f"lastUpdateTime gt {last_update_time}"

            if filter_query:
                params["$filter"] = filter_query  # type: ignore

            if skip:
                params["$skip"] = skip

        return self.ms_client.http_request(method="GET", url_suffix="api/incidents", timeout=timeout, params=params)

    @logger
    def update_incident(
        self,
        incident_id: int,
        status: Optional[str] = None,
        assigned_to: Optional[str] = None,
        classification: Optional[str] = None,
        determination: Optional[str] = None,
        tags: Optional[List[str]] = None,
        timeout: int = None,
        comment: Optional[str] = None,
    ) -> dict:
        """
         PATCH request to update single incident.
         Args:
             incident_id (int): incident's id
             status (str): Specifies the current status of the alert. Possible values are: (Active, Resolved, InProgress or
             Redirected)
             assigned_to (str): Owner of the incident.
             classification (str): Specification of the alert. Possible values are: InformationalExpectedActivity, FalsePositive,
              TruePositive.
             determination (str):  Specifies the determination of the alert. Possible values are: NotAvailable, Apt,
                                  Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, Other.
             tags (list): Custom tags associated with an incident. Separated by commas without spaces (CSV)
                  for example: tag1,tag2,tag3.
             timeout (int): The amount of time (in seconds) that a request will wait for a client to
                 establish a connection to a remote machine before a timeout occurs.
             comment (str): Comment to be added to the incident
        Returns( Dict): request results as dict:
                     { '@odata.context',
                       'value': updated incident,
                     }

        """
        body = assign_params(
            status=status,
            assignedTo=assigned_to,
            classification=classification,
            determination=determination,
            tags=tags,
            comment=comment,
        )
        if assigned_to == "":
            body["assignedTo"] = ""
        updated_incident = self.ms_client.http_request(
            method="PATCH", url_suffix=f"api/incidents/{incident_id}", json_data=body, timeout=timeout
        )
        return updated_incident

    @logger
    def get_incident(self, incident_id: int, timeout: int) -> dict:
        """
         GET request to get single incident.
         Args:
             incident_id (int): incident's id
             timeout (int): waiting time for command execution


        Returns( Dict): request results as dict:
                     { '@odata.context',
                       'value': updated incident,
                     }

        """
        incident = self.ms_client.http_request(method="GET", url_suffix=f"api/incidents/{incident_id}", timeout=timeout)
        return incident

    @logger
    def advanced_hunting(self, query: str, timeout: int):
        """
        POST request to the advanced hunting API:
        Args:
            query (str): query advanced hunting query language
            timeout (int): The amount of time (in seconds) that a request will wait for a client to
                     establish a connection to a remote machine before a timeout occurs.

        Returns:
            The response object contains three top-level properties:

                Stats - A dictionary of query performance statistics.
                Schema - The schema of the response, a list of Name-Type pairs for each column.
                Results - A list of advanced hunting events.
        """
        return self.ms_client.http_request(
            method="POST", url_suffix="api/advancedhunting/run", json_data={"Query": query}, timeout=timeout
        )


@logger
def start_auth(client: Client) -> CommandResults:
    result = client.ms_client.start_auth("!microsoft-365-defender-auth-complete")
    return CommandResults(readable_output=result)


@logger
def complete_auth(client: Client) -> CommandResults:
    client.ms_client.get_access_token()
    return CommandResults(readable_output="✅ Authorization completed successfully.")


@logger
def test_connection(client: Client) -> CommandResults:
    test_context_for_token(client)
    client.ms_client.get_access_token()  # If fails, MicrosoftApiModule returns an error
    return CommandResults(readable_output="✅ Success!")


""" HELPER FUNCTIONS """


def test_context_for_token(client: Client) -> None:
    """test_context_for_token
    Checks if the user acquired token via the authentication process.
    Args:
    Returns:

    """
    if client.client_credentials or client.ms_client.managed_identities_client_id:
        return
    if not (get_integration_context().get("access_token") or get_integration_context().get("current_refresh_token")):
        raise DemistoException(
            "This integration does not have a test module. Please run !microsoft-365-defender-auth-start and "
            "!microsoft-365-defender-auth-complete and check the connection using !microsoft-365-defender-auth-test"
        )


def test_module(client: Client) -> str:
    """Tests API connectivity and authentication'

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    :type client: ``Client``
    :param Client: client to use

    :return: 'ok' if test passed.
    :rtype: ``str``
    """
    # This  should validate all the inputs given in the integration configuration panel,
    # either manually or by using an API that uses them.
    if client.client_credentials:
        raise DemistoException(
            "When using a self-deployed configuration, run the !microsoft-365-defender-auth-test "
            "command in order to test the connection"
        )

    test_connection(client)

    return "ok"


def _format_user_entity(entity: dict[str, Any]) -> str:
    """
    Builds a readable user identity from a User entity, retaining the full identity
    (userPrincipalName or accountName) together with the domain, instead of only the domain name.

    Args:
        entity (dict): A single "User" entity from a Microsoft 365 Defender alert.

    Returns:
        str: A readable user identity, e.g. "username@domain (BOQDEVUSER.LOCAL)".
             Returns an empty string if no identifying information is present.
    """
    user = entity.get("userPrincipalName") or entity.get("accountName") or ""
    domain = entity.get("domainName") or ""
    if user and domain:
        return f"{user} ({domain})"
    return user or domain


def _get_impacted_entities(alerts_list: list[dict[str, Any]]) -> list[str]:
    """
    Collects the impacted user entities across all alerts, keeping the full user identity
    and preserving order while removing duplicates and empty values.

    Args:
        alerts_list (list): The alerts of a Microsoft 365 Defender incident.

    Returns:
        list: An ordered, de-duplicated list of readable user identities.
    """
    impacted_entities: list = []
    for alert in alerts_list:
        for entity in alert.get("entities") or []:
            if entity.get("entityType") != "User":
                continue
            formatted_user = _format_user_entity(entity)
            if formatted_user and formatted_user not in impacted_entities:
                impacted_entities.append(formatted_user)
    return impacted_entities


def _get_meta_data_for_incident(raw_incident: dict) -> dict:
    """
    Calculated metadata for the gicen incident
    Args:
        raw_incident (Dict): The incident as received from microsoft 365 defender

    Returns: Dictionary with the calculated data

    """
    if not raw_incident:
        raw_incident = {}

    alerts_list = raw_incident.get("alerts", [])

    alerts_status = [alert.get("status") for alert in alerts_list if alert.get("status")]
    first_activity_list = [alert.get("firstActivity") for alert in alerts_list]
    last_activity_list = [alert.get("lastActivity") for alert in alerts_list]

    mailboxes_set = set()
    mailboxes = []
    for alert in alerts_list:
        for entity in alert.get("entities", []):
            if entity.get("entityType") == "Mailbox" and entity.get("mailboxAddress", "") not in mailboxes_set:
                mailboxes.append(
                    {"Mailbox": entity.get("mailboxAddress", ""), "Display Name": entity.get("mailboxDisplayName", "")}
                )
                mailboxes_set.add(entity.get("mailboxAddress", ""))

    return {
        "Categories": [alert.get("category", "") for alert in alerts_list],
        "Impacted entities": _get_impacted_entities(alerts_list),
        "Active alerts": f'{alerts_status.count("Active") + alerts_status.count("New")} / {len(alerts_status)}',
        "Service sources": list({alert.get("serviceSource", "") for alert in alerts_list}),
        "Detection sources": list({alert.get("detectionSource", "") for alert in alerts_list}),
        "First activity": str(min(first_activity_list, key=lambda x: dateparser.parse(x))) if alerts_list else "",  # type: ignore
        "Last activity": str(max(last_activity_list, key=lambda x: dateparser.parse(x))) if alerts_list else "",  # type: ignore
        "Devices": [
            {
                "device name": device.get("deviceDnsName", ""),
                "risk level": device.get("riskScore", ""),
                "tags": ",".join(device.get("tags", [])),
            }
            for alert in alerts_list
            for device in alert.get("devices", [])
        ],
        "Mailboxes": mailboxes,
        "comments": raw_incident.get("comments", []),
    }


def convert_incident_to_readable(raw_incident: dict) -> dict:
    """
    Converts incident received from microsoft 365 defender to readable format
    Args:
        raw_incident (Dict): The incident as received from microsoft 365 defender

    Returns: new dictionary with keys mapping.

    """
    if not raw_incident:
        raw_incident = {}

    incident_meta_data = _get_meta_data_for_incident(raw_incident)
    device_groups = {device.get("device name") for device in incident_meta_data.get("Devices", []) if device.get("device name")}
    return {
        "Incident name": raw_incident.get("incidentName"),
        "Tags": ", ".join(raw_incident.get("tags", [])),
        "Severity": raw_incident.get("severity"),
        "Incident ID": raw_incident.get("incidentId"),
        # investigation state - relevant only for alerts.
        "Categories": ", ".join(set(incident_meta_data.get("Categories", []))),
        "Impacted entities": ", ".join(incident_meta_data.get("Impacted entities", [])),
        "Impacted mailboxes": ", ".join(mailbox.get("Mailbox") for mailbox in incident_meta_data.get("Mailboxes", [])),
        "Active alerts": incident_meta_data.get("Active alerts", []),
        "Service sources": ", ".join(incident_meta_data.get("Service sources", [])),
        "Detection sources": ", ".join(incident_meta_data.get("Detection sources", [])),
        # Data sensitivity - is not relevant
        "First activity": incident_meta_data.get("First activity", ""),
        "Last activity": incident_meta_data.get("Last activity", ""),
        "Status": raw_incident.get("status"),
        "Assigned to": raw_incident.get("assignedTo", "Unassigned"),
        "Classification": raw_incident.get("classification", "Not set"),
        "Device groups": ", ".join(device_groups),
    }


def _get_default_incident_close_out_or_reactivation_reason(delta: dict):
    """
    Get the default incident close reason for the given classification.
    Args:
        delta (dict): The delta of the incident to update.
    """
    if delta.get("closeReason") == "FalsePositive" and delta.get("classification") != "FalsePositive":
        delta.update({"classification": "FalsePositive", "determination": "Other"})
        demisto.debug("Microsoft Defender 365 - Updating classification and determination to FalsePositive and Other")
    elif delta.get("closeReason") == "Other" or delta.get("closeReason") == "Duplicate":
        delta.update({"classification": "Unknown", "determination": "NotAvailable"})
        demisto.debug("Microsoft Defender 365 - Updating classification and determination to Unknown and NotAvailable")


def _parse_classification_mapping(mapped_value: str) -> tuple[str, str]:
    """
    Safely parse a mapping value in the format 'Classification-Determination'.

    Falls back to safe defaults if format is invalid.
    """
    if not mapped_value:
        return "Unknown", "NotAvailable"

    parts = mapped_value.split("-", 1)  # split only once

    if len(parts) != 2 or not parts[0] or not parts[1]:
        demisto.debug(
            f"Microsoft Defender 365 - Invalid classification mapping format: '{mapped_value}'. "
            "Falling back to Unknown/NotAvailable."
        )
        return "Unknown", "NotAvailable"

    return parts[0], parts[1]


def _get_default_modified_incidents_close_or_reopen_entries_reason(incident: dict) -> dict:
    """
    Get the default incident close reason for the given classification.
    Args:
        incident (dict): The incident which holds the classification.

    Returns:
        dict: A dictionary containing the default close or reopen entries reason.
    """
    return {
        "Type": EntryType.NOTE,
        "Contents": {
            "dbotIncidentClose": True,
            "closeReason": MICROSOFT_RESOLVED_CLASSIFICATION_TO_XSOAR_CLOSE_REASON.get(
                incident.get("classification", "Unknown"), "Other"
            ),
        },
        "ContentsFormat": EntryFormat.JSON,
    }


""" COMMAND FUNCTIONS """


@logger
def microsoft_365_defender_incidents_list_command(client: Client, args: dict) -> CommandResults:
    """
    Returns list of the latest incidents in microsoft 365 defender in readable table.
    The list can be filtered using the following arguments:
        - limit (int) - number of incidents in the list, integer between 0 to 100.
        - status (str) - fetch only incidents with the given status.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments:
              - limit (int) - integer between 0 to 100
              - status (str) - get incidents with the given status (Active, Resolved or Redirected)
              - assigned_to (str) - get incidents assigned to the given user
              - offset (int) - skip the first N entries of the list
              - odata (str) - json dictionary containing odata uri parameters
    Returns: CommandResults

    """
    limit = arg_to_number(args.get("limit", MAX_ENTRIES), arg_name="limit", required=True)
    status = args.get("status")
    assigned_to = args.get("assigned_to")
    offset = arg_to_number(args.get("offset"))
    timeout = arg_to_number(args.get("timeout", TIMEOUT))
    odata = args.get("odata")

    if odata:
        try:
            odata = json.loads(odata)
        except json.JSONDecodeError:
            return_error(f"Can't parse odata argument as JSON array.\nvalue: {odata}")

    response = client.incidents_list(
        limit=limit, status=status, assigned_to=assigned_to, skip=offset, timeout=timeout, odata=odata
    )

    raw_incidents = response.get("value")
    readable_incidents = [convert_incident_to_readable(incident) for incident in raw_incidents]
    if readable_incidents:
        headers = list(readable_incidents[0].keys())  # the table headers are the incident keys.
        human_readable = tableToMarkdown(name="Incidents:", t=readable_incidents, headers=headers)

    else:
        human_readable = "No incidents found"

    return CommandResults(
        outputs_prefix="Microsoft365Defender.Incident",
        outputs_key_field="incidentId",
        outputs=raw_incidents,
        readable_output=human_readable,
    )


def get_determination_value(classification: Optional[str], determination: Optional[str]) -> Optional[str]:
    """
    Determines the appropriate determination value based on the provided classification and determination inputs.

    Args:
        classification (Optional[str]): The classification of the incident. Must be a valid key in
            the CLASSIFICATION_DETERMINATION_MAPPING dictionary.
        determination (Optional[str]): The determination value associated with the classification. Must be
            a valid value in the corresponding list of determination values for the given classification.

    Returns:
        Optional[str]:
            - If both classification and determination are `None`, returns `None`.
            - If classification is valid but determination is not provided:
                - Returns "NotAvailable" if the classification is "Unknown".
                - Returns "Other" for all other classifications.
            - If both classification and determination are valid, returns the provided determination.

    Raises:
        DemistoException:
            - If classification is not provided or not valid (not in CLASSIFICATION_DETERMINATION_MAPPING).
            - If determination is provided but is not valid for the given classification.

    """
    if classification is None and determination is None:
        return None

    if not classification or classification not in CLASSIFICATION_DETERMINATION_MAPPING:
        raise DemistoException("get_determination_value - Please provide a valid classification")

    if not determination:
        return "NotAvailable" if classification == "Unknown" else "Other"

    if determination not in CLASSIFICATION_DETERMINATION_MAPPING[classification]:
        raise DemistoException(
            f"get_determination_value - Invalid determination. "
            f"Please provide one of the following: {CLASSIFICATION_DETERMINATION_MAPPING[classification]}"
        )

    return determination


@logger
def microsoft_365_defender_incident_update_command(client: Client, args: dict) -> CommandResults:
    """
    Update an incident.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments:
              - id (int) - incident's id (required)
              - status (str) - Specifies the current status of the alert. Possible values are: (Active, Resolved, InProgress or
              Redirected)
              - assigned_to (str) - Owner of the incident.
              - classification (str) - Specification of the alert. Possible values are: Unknown, FalsePositive, TruePositive.
              - determination (str) -  Specifies the determination of the alert. Possible values are: NotAvailable, Apt,
                                 Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, Other.
              - tags - Custom tags associated with an incident. Separated by commas without spaces (CSV)
                       for example: tag1,tag2,tag3.

    Returns: CommandResults
    """
    raw_tags = args.get("tags")
    tags = raw_tags.split(",") if raw_tags else None
    status = args.get("status")
    assigned_to = args.get("assigned_to")
    classification = args.get("classification")
    determination = get_determination_value(classification, args.get("determination"))
    incident_id = arg_to_number(args.get("id"))
    timeout = arg_to_number(args.get("timeout", TIMEOUT))
    comment = args.get("comment")

    updated_incident = client.update_incident(
        incident_id=incident_id,
        status=status,
        assigned_to=assigned_to,
        classification=classification,
        determination=determination,
        tags=tags,
        timeout=timeout,
        comment=comment,
    )
    if updated_incident.get("@odata.context"):
        del updated_incident["@odata.context"]

    readable_incident = convert_incident_to_readable(updated_incident)
    human_readable_table = tableToMarkdown(
        name=f"Updated incident No. {incident_id}:", t=readable_incident, headers=list(readable_incident.keys())
    )

    return CommandResults(
        outputs_prefix="Microsoft365Defender.Incident",
        outputs_key_field="incidentId",
        outputs=updated_incident,
        readable_output=human_readable_table,
    )


@logger
def microsoft_365_defender_incident_get_command(client: Client, args: dict) -> CommandResults:
    """
    Get an incident.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments:
              - id (int)        - incident's id (required)
              - timeout (int)   - waiting time for command execution

    Returns: CommandResults
    """
    incident_id = arg_to_number(args.get("id"))
    timeout = arg_to_number(args.get("timeout", TIMEOUT))

    incident = client.get_incident(incident_id=incident_id, timeout=timeout)
    if incident.get("@odata.context"):
        del incident["@odata.context"]

    readable_incident = convert_incident_to_readable(incident)
    human_readable_table = tableToMarkdown(
        name=f"Incident No. {incident_id}:", t=readable_incident, headers=list(readable_incident.keys())
    )

    return CommandResults(
        outputs_prefix="Microsoft365Defender.Incident",
        outputs_key_field="incidentId",
        outputs=incident,
        readable_output=human_readable_table,
    )


@logger
def fetch_incidents(
    client: Client, mirroring_fields: dict, first_fetch_time: str, fetch_limit: int, timeout: int = None
) -> List[dict]:
    """
    Uses to fetch incidents into Demisto
    Documentation: https://xsoar.pan.dev/docs/integrations/fetching-incidents#the-fetch-incidents-command

    Due to API limitations (The incidents are not ordered and there is a limit of 100 incident for each request),
    We get all the incidents newer than last_run/first_fetch_time and saves them in a queue. The queue is sorted by
    creation date of each incident.

    As long as the queue has more incidents than fetch_limit the function will return the oldest incidents in the queue.
    If the queue is smaller than fetch_limit we fetch more incidents with the same logic described above.


    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        mirroring_fields(dict): The required fields to add to an incident to enable mirroring.
        first_fetch_time(str): From when to fetch if first time, e.g. `3 days`.
        fetch_limit(int): The number of incidents in each fetch.
        timeout(int): The time limit in seconds for this function to run.
            Note: exceeding the time doesnt kill the function just prevents the next api call.
    Returns:
        incidents, new last_run
    """

    start_time = time.time()
    test_context_for_token(client)

    last_run_dict = demisto.getLastRun()

    last_run = last_run_dict.get("last_run")
    if not last_run:  # this is the first run
        first_fetch_date_time = dateparser.parse(first_fetch_time)
        if not first_fetch_date_time:
            raise DemistoException(
                f"Microsoft Defender 365 Fetch incidents - Could not parse the first_fetch_time: {first_fetch_time=}"
            )
        last_run = first_fetch_date_time.strftime(DATE_FORMAT)

    # creates incidents queue
    incidents_queue = last_run_dict.get("incidents_queue", [])

    if len(incidents_queue) < fetch_limit:
        incidents = []

        # The API is limited to MAX_ENTRIES incidents for each requests, if we are trying to get more than MAX_ENTRIES
        # incident we skip (offset) the number of incidents we already fetched.
        offset = 0

        # This loop fetches all the incidents that had been created after last_run, due to API limitations the fetching
        # occurs in batches. If timeout was given, exceeding the time will result an error.
        # Note: Because the list is cannot be ordered by creation date, reaching timeout will force to re-run this
        #       function from the start.

        while True:
            time_delta = time.time() - start_time
            if timeout and time_delta > timeout:
                raise DemistoException("Fetch incidents - Time out. Please change first_fetch parameter to be more recent one")

            # HTTP request
            response = client.incidents_list(from_date=last_run, skip=offset, timeout=timeout)
            raw_incidents = response.get("value")
            for incident in raw_incidents:
                incident.update(_get_meta_data_for_incident(incident))
                incident.update(mirroring_fields)

            incidents += [
                {
                    "name": f"Microsoft 365 Defender {incident.get('incidentId')}",
                    "occurred": incident.get("createdTime"),
                    "rawJSON": json.dumps(incident),
                }
                for incident in raw_incidents
            ]

            # raw_incidents length is less than MAX_ENTRIES than we fetch all the relevant incidents
            if len(raw_incidents) < int(MAX_ENTRIES):
                break
            offset += int(MAX_ENTRIES)

        # sort the incidents by the creation time
        incidents.sort(key=lambda x: dateparser.parse(x["occurred"]))  # type: ignore
        incidents_queue += incidents

    oldest_incidents = incidents_queue[:fetch_limit]
    new_last_run = incidents_queue[-1]["occurred"] if oldest_incidents else last_run  # newest incident creation time
    demisto.setLastRun({"last_run": new_last_run, "incidents_queue": incidents_queue[fetch_limit:]})
    return oldest_incidents


def _query_set_limit(query: str, limit: int) -> str:
    """
    Add limit to given query. If the query has limit, changes it.
    This function now properly handles complex queries with parentheses, unions, and joins.

    Args:
        query: the original query
        limit: new limit value, if the value is negative return the original query.
    Returns: query with limit parameters
    """
    if limit < 0:
        return query

    # Check if query already has a limit or take at the top level (not inside parentheses)
    # We'll use a simple approach: only add/modify limit at the top level (outside parentheses)

    def get_top_level_pipes(query_str: str) -> list:
        """Split query by pipes that are at parenthesis level 0 (top level)."""
        parts: list[str] = []
        current_part: list[str] = []
        paren_depth = 0

        i = 0
        while i < len(query_str):
            char = query_str[i]
            next_char = query_str[i + 1] if i + 1 < len(query_str) else ""
            prev_char = query_str[i - 1] if i > 0 else ""

            if char == "(":
                paren_depth += 1
            elif char == ")":
                paren_depth -= 1

            # Handle pipes (avoid splitting on ||)
            is_pipe = char == "|" and next_char != "|" and prev_char != "|"

            if is_pipe and paren_depth == 0:
                parts.append("".join(current_part))
                current_part = []
            else:
                current_part.append(char)

            i += 1

        # Add the last part
        if current_part:
            parts.append("".join(current_part))

        return parts

    # Split query into top-level sections
    query_parts = get_top_level_pipes(query)

    # Check if any top-level section starts with limit or take (case-insensitive)
    changed = False
    for i, part in enumerate(query_parts):
        stripped = part.strip()
        if stripped.lower().startswith(("limit ", "take ")):
            # Replace this section with new limit
            query_parts[i] = f" limit {limit} "
            changed = True

    # If no limit found, append it
    if not changed:
        query_parts.append(f" limit {limit} ")

    # Rejoin with pipes (using single pipe to match original behavior)
    fixed_query = "|".join(query_parts)
    return fixed_query


@logger
def microsoft_365_defender_advanced_hunting_command(client: Client, args: dict) -> CommandResults:
    """
    Sends a query for the advanced hunting tool.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments:
              - query (str) - The query to run (required)
              - limit (int) - number of entries in the result, -1 for no limit.
    Returns:

    """
    query = args.get("query", "")
    limit = arg_to_number(args.get("limit", "-1"))
    timeout = arg_to_number(args.get("timeout", TIMEOUT))

    query = _query_set_limit(query, limit)  # type: ignore

    response = client.advanced_hunting(query=query, timeout=timeout)
    results = response.get("Results")
    schema = response.get("Schema", {})
    headers = [item.get("Name") for item in schema]
    context_result = {"query": query, "results": results}
    human_readable_table = tableToMarkdown(name=f" Result of query: {query}:", t=results, headers=headers)
    return CommandResults(
        outputs_prefix="Microsoft365Defender.Hunt",
        outputs_key_field="query",
        outputs=context_result,
        readable_output=human_readable_table,
    )


""" MIRRORING COMMANDS """


def fetch_modified_incident_ids(client: Client, last_update_time: str) -> List[str]:
    """
    Fetches the incident ids that had been created after last_run and filters their ids.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        last_update_time(str): last update time of the mirroring to fetch incidents that were modified after this time.
    Returns: List of incident ids.
    """

    demisto.debug("Microsoft Defender 365 - Starting fetch_modified_incident_ids")
    incidents = []

    # The API is limited to MAX_ENTRIES incidents for each requests, if we are trying to get more than MAX_ENTRIES
    # incident we skip (offset) the number of incidents we already fetched.
    offset = 0

    # This loop fetches all the incidents that had been created after last_run, due to API limitations the fetching
    # occurs in batches. If timeout was given, exceeding the time will result an error.

    while True:
        # HTTP request
        demisto.debug(f"Microsoft Defender 365 - fetch_modified_incident_ids - fetching incidents with offset {offset}")
        response = client.incidents_list(timeout=TIMEOUT_INT, last_update_time=last_update_time, skip=offset)
        raw_incidents = response.get("value")
        demisto.debug(f"Microsoft Defender 365 - fetch_modified_incident_ids - fetched {len(raw_incidents)}")
        for incident in raw_incidents:
            incidents.append(str(incident.get(MICROSOFT_INCIDENT_ID_KEY)))
        # raw_incidents length is less than MAX_ENTRIES than we fetch all the relevant incidents
        if len(raw_incidents) < int(MAX_ENTRIES):
            break
        offset += int(MAX_ENTRIES)
    demisto.debug(f"Microsoft Defender 365 - fetch_modified_incident_ids - returning incident ids {incidents}")
    return incidents


def get_modified_incidents_close_or_repopen_entries(modified_incidents: List[dict], close_incident: bool) -> List[dict]:
    """
    Get the entries for closing or reopening incidents.
    Args:
        modified_incidents (List[dict]): The modified incidents.
        close_incident (bool): Whether resolved incidents from Microsoft should be closed in XSOAR in the mirroring in process.
    Returns:
        List[dict]: The entries for closing or reopening incidents.
    """
    demisto.debug("Microsoft Defender 365 - Starting get_modified_incidents_close_or_repopen_entries")
    entries = []
    if close_incident:
        custom_close_reason = demisto.params().get("custom_defender_to_xsoar_close_reason", False)
        for incident in modified_incidents:
            if incident.get("status") in ("Resolved", "Redirected"):
                demisto.debug(
                    f"Microsoft Defender 365 - incident {incident.get(MICROSOFT_INCIDENT_ID_KEY)} is "
                    f"{(incident.get('status') or '').lower()} in Microsoft, "
                    f"adding close entry to XSOAR."
                )
                if custom_close_reason:
                    # Reading custom Defender->XSOAR close-reason mapping.
                    custom_defender_to_xsoar_close_reason_mapping = comma_separated_mapping_to_dict(
                        demisto.params().get("custom_defender_to_xsoar_close_reason_mapping", "")
                    )

                    # Overriding default close-reason mapping if there exists a custom one.
                    incident_close_reason = incident.get("classification")
                    demisto.debug(
                        f"mapping is {custom_defender_to_xsoar_close_reason_mapping} "
                        f"and classification is {incident_close_reason}"
                    )
                    if incident_close_reason in custom_defender_to_xsoar_close_reason_mapping:
                        entry = {
                            "Type": EntryType.NOTE,
                            "Contents": {
                                "dbotIncidentClose": True,
                                "closeReason": custom_defender_to_xsoar_close_reason_mapping.get(incident_close_reason, "Other"),
                            },
                            "ContentsFormat": EntryFormat.JSON,
                        }
                    else:
                        entry = _get_default_modified_incidents_close_or_reopen_entries_reason(incident)
                else:
                    entry = _get_default_modified_incidents_close_or_reopen_entries_reason(incident)
            else:
                demisto.debug(
                    f"Microsoft Defender 365 - incident {incident.get(MICROSOFT_INCIDENT_ID_KEY)} "
                    f"is not Resolved in Microsoft, adding reopen entry to XSOAR."
                )
                entry = {"dbotIncidentReopen": True}
            entries.append(entry)
    return entries


def get_modified_remote_data_command(client: Client, args: dict) -> GetModifiedRemoteDataResponse:
    """
    Get the modified remote data.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments.

    Returns:
        GetModifiedRemoteDataResponse: The modified remote data.
    """
    remote_args = GetModifiedRemoteDataArgs(args)
    parsed_date = dateparser.parse(remote_args.last_update, settings={"TIMEZONE": "UTC"})
    if not parsed_date:
        demisto.error(f"Microsoft Defender 365 get_modified_remote_data_command - Could not parse {remote_args.last_update=}")
        return GetModifiedRemoteDataResponse(modified_incident_ids=[])

    last_update = parsed_date.strftime(DATE_FORMAT)
    demisto.debug(f"Microsoft Defender 365 - Last update: {last_update}")
    modified_incident_ids = fetch_modified_incident_ids(client, last_update)
    return GetModifiedRemoteDataResponse(modified_incident_ids=modified_incident_ids)


def fetch_modified_incident(client: Client, incident_id: int) -> dict:
    """
    Fetches the modified incident from Microsoft 365 Defender.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        incident_id(int): The incident ID.
    Returns:
        dict: The modified incident.
    """
    demisto.debug(f"Microsoft Defender 365 - Starting fetch_modified_incident {incident_id=}")
    incident = client.get_incident(incident_id=incident_id, timeout=TIMEOUT_INT)
    demisto.debug(f"Microsoft Defender 365 - Fetched incident {incident_id=}")
    if incident.get("@odata.context"):
        del incident["@odata.context"]
    incident.update(_get_meta_data_for_incident(incident))
    demisto.debug(f"Microsoft Defender 365 - Updated incident {incident_id=} with metadata.")
    return incident


def get_entries_for_comments(comments: List[dict[str, str]], last_update: datetime, comment_tag: str) -> List[dict]:
    """
    Get the entries for the comments of the incident.
    Args:
        comments (List[dict]): The comments of the incident from Microsoft 365 Defender.
        last_update (datetime): The last run time of the mirroring - should bring in comments that were added afterward to
         avoid entries duplicates.
        comment_tag (str): The tag to add for the comments entries when mirroring in.

    Returns:
        List[dict]: The entries for the comments.
    """
    entries = []
    for comment in comments:
        # check if the comment is a mirrored out entry to ms fto avoid duplicates
        if MIRRORED_OUT_XSOAR_ENTRY_TO_MICROSOFT_COMMENT_INDICATOR not in comment.get("comment", ""):
            comment_time = dateparser.parse(comment.get("createdTime", ""))
            if not comment_time:
                demisto.error(f"Microsoft Defender 365 Could not parse {comment.get('createdTime')} for {comment=}")
                continue
            if comment_time > last_update:
                entries.append(
                    {
                        "Type": EntryType.NOTE,
                        "Contents": f"Created By: {comment.get('createdBy', '')}\n"
                        f"Created On: {comment.get('createdTime', '')}\n"
                        f"{comment.get('comment', '')}",
                        "ContentsFormat": EntryFormat.HTML,
                        "Tags": [comment_tag],
                        "Note": True,
                        "EntryContext": {"comments": comment},
                    }
                )
    return entries


def get_incident_entries(
    mirrored_object: dict, last_update: datetime, comment_tag: str, close_incident: bool = False
) -> List[dict]:
    """
    Creates the entries for the mirrored in incident.
    Args:
        mirrored_object (dict): The incident object from Microsoft 365 Defender.
        last_update (datetime): The last run time of the mirroring - should bring in comments that were added afterward to avoid
         entries duplicates.
        comment_tag (str): The tag to add for the comments entries when mirroring in.
        new_incident (bool): Whether the incident is new a- was just fetched therefore should bring in all the comments.
        close_incident (bool): Whether resolved incidents from Microsoft should be closed in XSOAR in the mirroring in process.

    Returns:
        List[dict]: The entries for the incident.
    """
    entries = get_modified_incidents_close_or_repopen_entries([mirrored_object], close_incident=close_incident)
    entries.extend(get_entries_for_comments(mirrored_object.get("comments", []), last_update, comment_tag))
    return entries


def get_remote_data_command(client: Client, args: dict[str, Any]) -> GetRemoteDataResponse:
    """
    Get the remote incident data from Microsoft 365 Defender to mirror in XSOAR and create the entries for the incident.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments.
    Returns:
        GetRemoteDataResponse: The remote data.

    """
    remote_args = GetRemoteDataArgs(args)
    remote_incident_id = arg_to_number(remote_args.remote_incident_id)
    if remote_incident_id is None:
        raise DemistoException("get_remote_data_command - remote_incident_id is None")
    comment_tag = demisto.params().get("comment_tag_from_microsoft365defender", "CommentFromMicrosoft365Defender")
    last_update = dateparser.parse(remote_args.last_update, settings={"TIMEZONE": "UTC"})
    if not last_update:
        demisto.error(f"Microsoft Defender 365 get_remote_data_command - Could not parse {remote_args.last_update=}")
        return GetRemoteDataResponse({MICROSOFT_INCIDENT_ID_KEY: remote_incident_id}, entries=[])
    close_incident = argToBoolean(demisto.params().get("close_incident", False))

    mirrored_object: Dict = {}
    demisto.debug(
        f"Microsoft Defender 365 - Performing get-remote-data command with incident id: {remote_incident_id} and "
        f"last_update: {remote_args.last_update}"
    )
    try:
        mirrored_object = fetch_modified_incident(client, remote_incident_id)
        entries = get_incident_entries(mirrored_object, last_update, comment_tag, close_incident=close_incident)
        return GetRemoteDataResponse(mirrored_object, entries)
    except Exception as e:
        demisto.debug(
            f"Error in Microsoft incoming mirror for incident: {remote_incident_id}\n"
            f"Error message: {e!s} \n {traceback.format_exc()}"
        )

        if not mirrored_object:
            mirrored_object = {MICROSOFT_INCIDENT_ID_KEY: remote_incident_id}
        mirrored_object["in_mirror_error"] = str(e)
        return GetRemoteDataResponse(mirrored_object, entries=[])


def get_mapping_fields_command():
    """
    Get the mapping fields for the incident type.
    Returns:
        GetMappingFieldsResponse: The mapping fields.
    """
    incident_type_scheme = SchemeTypeMapping(type_name="Microsoft 365 Defender Incident")
    demisto.debug("Microsoft Defender 365 - collecting incident mapping for Microsoft 365 Defender Incident type.")
    for name, description in OUTGOING_MIRRORED_FIELDS.items():
        incident_type_scheme.add_field(name=name, description=description)
    mapping_response = GetMappingFieldsResponse()
    mapping_response.add_scheme_type(incident_type_scheme)
    return mapping_response


def handle_incident_close_out_or_reactivation(delta: dict, incident_status: IncidentStatus):
    """
    Handle the incident close out. If the close out is enabled, the incident will be resolved and the classification and
    determination will be updated according to the close reason.

    Args:
        delta (dict): The delta of the incident to update.
        incident_status (IncidentStatus): The incident status in XSOAR.

    """
    demisto.debug("Microsoft Defender 365 - Starting handle_incident_close_out_or_reactivation")
    # Only close or reopen microsoft incidents based on xsoar if close_out is enabled.
    if not argToBoolean(demisto.params().get("close_out", False)):
        demisto.debug("Microsoft Defender 365 - Close out is not enabled")
        return
    demisto.debug("Microsoft Defender 365 - Close out is enabled")
    if incident_status == IncidentStatus.DONE:
        if any(delta.get(key) for key in ["closeReason", "closeNotes", "closingUserId"]):
            delta["status"] = "Resolved"
            demisto.debug("Microsoft Defender 365 - Updating incident status to Resolved")
            # this functionality awaits https://jira-dc.paloaltonetworks.com/browse/CRTX-151123?filter=-2
            # once resolved the microsoft365classification field needs to be returned to the close form in order to get
            # the classification and determination fields in delta.

            if demisto.params().get("custom_xsoar_to_defender_close_reason", False):
                # Reading custom XSOAR->Defender close-reason mapping.
                custom_xsoar_to_defender_close_reason_mapping = comma_separated_mapping_to_dict(
                    demisto.params().get("custom_xsoar_to_defender_close_reason_mapping", "")
                )

                # Overriding default close-reason mapping if there exists a custom one.
                incident_close_reason = delta.get("closeReason")
                demisto.debug(f"mapping is {custom_xsoar_to_defender_close_reason_mapping} and status is {incident_close_reason}")
                if incident_close_reason in custom_xsoar_to_defender_close_reason_mapping:
                    defender_close_reason_candidate = custom_xsoar_to_defender_close_reason_mapping.get(incident_close_reason)
                    # Transforming resolved close-reason to match Defender format.
                    defender_classification, defender_determination = _parse_classification_mapping(
                        defender_close_reason_candidate
                    )
                    demisto.debug(
                        f"Resolving Defender incident with classification"
                        f"{defender_classification} and determination {defender_determination}"
                    )
                    delta.update({"classification": defender_classification, "determination": defender_determination})
                else:
                    demisto.debug("resolve_defender_close_reason using default mapping")
                    _get_default_incident_close_out_or_reactivation_reason(delta)

            else:
                _get_default_incident_close_out_or_reactivation_reason(delta)
    else:
        if any(delta.get(key) == "" for key in ["closeReason", "closeNotes", "closingUserId"]):
            delta["status"] = "Active"
            demisto.debug("Microsoft Defender 365 - Updating incident status to Active")


def mirror_out_entries(client: Client, entries: list[Dict], comment_tag: str, remote_incident_id: int):
    """
    Mirror out the entries to Microsoft 365 Defender.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        entries (list[Dict]): The entries added to the incident.
        comment_tag (str): The comment tag to identify the entries that should be mirrored out as comments.
        remote_incident_id (int): The remote incident ID.


    """
    demisto.debug("Microsoft Defender 365 - Starting mirror_out_entries")
    for entry in entries:
        demisto.debug(f'handling entry {entry.get("id")}, type: {entry.get("type")}, tags: {entry.get("tags", [])}')
        tags = entry.get("tags", [])
        user = entry.get("user", "dbot") or "dbot"
        if comment_tag in tags:
            if str(entry.get("format")) == "html":
                contents = str(entry.get("contents", ""))
                text = (
                    f"({user}): <br/><br/>[code]{contents} <br/><br/>[/code]"
                    f" {MIRRORED_OUT_XSOAR_ENTRY_TO_MICROSOFT_COMMENT_INDICATOR}"
                )
            else:
                text = f"({user}): {entry.get('contents', '')!s}\n\n {MIRRORED_OUT_XSOAR_ENTRY_TO_MICROSOFT_COMMENT_INDICATOR}"

            client.update_incident(incident_id=remote_incident_id, timeout=TIMEOUT_INT, comment=text)
            demisto.debug(f"Microsoft Defender 365 - mirror out entries updated incident {remote_incident_id=}")

        else:
            demisto.debug(f"Microsoft Defender 365 - Skipping entry {entry.get('id')} as it does not contain the comment tag")


def update_remote_system_command(client: Client, args: Dict[str, Any]) -> str:
    """
    Update the remote system in Microsoft 365 Defender - Mirror out the entries and update the incident.
    Args:
        client(Client): Microsoft 365 Defender's client to preform the API calls.
        args(Dict): Demisto arguments.
    Returns:
        str: The remote incident ID.
    """
    update_remote_system_args = UpdateRemoteSystemArgs(args)
    remote_incident_id = update_remote_system_args.remote_incident_id
    delta = update_remote_system_args.delta

    demisto.debug(
        f"Microsoft Defender 365 -  update_remote_system_command {remote_incident_id=} \n {delta=} \n  "
        f"{update_remote_system_args.incident_changed=} \n {update_remote_system_args.inc_status=}"
    )

    try:
        if update_remote_system_args.incident_changed and delta:
            handle_incident_close_out_or_reactivation(delta, update_remote_system_args.inc_status)

            client.update_incident(
                incident_id=remote_incident_id,
                status=delta.get("status"),
                assigned_to=delta.get("assignedTo"),
                classification=delta.get("classification"),
                determination=delta.get("determination"),
                tags=argToList(delta.get("tags")),
                timeout=TIMEOUT_INT,
                comment=delta.get("comment"),
            )

            demisto.debug(f"Microsoft Defender 365 - Updated incident {remote_incident_id=}")
        else:
            demisto.debug(
                f"Microsoft Defender 365 -  Skipping updating remote incident fields [{remote_incident_id}] as it is not new "
                f"nor changed"
            )

        if update_remote_system_args.entries:
            comment_tag = demisto.params().get("comment_tag", COMMENT_TO_MICROSOFT_DEFAULT_TAG)
            demisto.debug("Microsoft Defender 365 - new entries were found in the xsoar incident.")
            mirror_out_entries(client, update_remote_system_args.entries, comment_tag, remote_incident_id)

        return remote_incident_id

    except Exception as e:
        demisto.error(
            f"Microsoft Defender 365 - Error in outgoing mirror for incident {remote_incident_id} \nError message: {e!s}"
        )
        return remote_incident_id


""" MAIN FUNCTION """


def main() -> None:
    """main function, parses params and runs command functions

    :return:
    :rtype:
    """
    params = demisto.params()
    # if your Client class inherits from BaseClient, SSL verification is
    # handled out of the box by it, just pass ``verify_certificate`` to
    # the Client constructor
    verify_certificate = not params.get("insecure", False)

    # if your Client class inherits from BaseClient, system proxy is handled
    # out of the box by it, just pass ``proxy`` to the Client constructor
    proxy = params.get("proxy", False)
    app_id = params.get("creds_client_id", {}).get("password", "") or params.get("app_id") or params.get("_app_id")
    base_url = params.get("base_url")

    tenant_id = params.get("creds_tenant_id", {}).get("password", "") or params.get("tenant_id") or params.get("_tenant_id")
    client_credentials = params.get("client_credentials", False)
    enc_key = (params.get("credentials") or {}).get("password") or params.get("enc_key")
    certificate_thumbprint = params.get("creds_certificate", {}).get("identifier", "") or params.get("certificate_thumbprint", "")

    private_key = replace_spaces_in_credential(params.get("creds_certificate", {}).get("password", "")) or params.get(
        "private_key", ""
    )
    managed_identities_client_id = get_azure_managed_identities_client_id(params)

    first_fetch_time = params.get("first_fetch", "3 days").strip()
    fetch_limit = arg_to_number(params.get("max_fetch", 10))
    fetch_timeout = arg_to_number(params.get("fetch_timeout", TIMEOUT))

    mirroring_fields = {
        "mirror_direction": MIRROR_DIRECTION.get(params.get("mirror_direction", "None")),
        "mirror_instance": demisto.integrationInstance(),
        "mirror_tags": [params.get("comment_tag", COMMENT_TO_MICROSOFT_DEFAULT_TAG)],
    }

    demisto.debug(f"Command being called is {demisto.command()}")

    command = demisto.command()
    args = demisto.args()

    demisto.debug(f"{args=}, \n{mirroring_fields}")

    try:
        if not managed_identities_client_id and not app_id:
            raise Exception("Application ID must be provided.")

        client = Client(
            app_id=app_id,
            verify=verify_certificate,
            base_url=base_url,
            proxy=proxy,
            tenant_id=tenant_id,
            enc_key=enc_key,
            client_credentials=client_credentials,
            certificate_thumbprint=certificate_thumbprint,
            private_key=private_key,
            managed_identities_client_id=managed_identities_client_id,
        )
        if demisto.command() == "test-module":
            # This is the call made when pressing the integration Test button.
            return_results(test_module(client))

        elif command == "microsoft-365-defender-auth-start":
            return_results(start_auth(client))

        elif command == "microsoft-365-defender-auth-complete":
            return_results(complete_auth(client))

        elif command == "microsoft-365-defender-auth-reset":
            return_results(reset_auth())

        elif command == "microsoft-365-defender-auth-test":
            return_results(test_connection(client))

        elif command == "microsoft-365-defender-incidents-list":
            test_context_for_token(client)
            return_results(microsoft_365_defender_incidents_list_command(client, args))

        elif command == "microsoft-365-defender-incident-update":
            test_context_for_token(client)
            return_results(microsoft_365_defender_incident_update_command(client, args))

        elif command == "microsoft-365-defender-advanced-hunting":
            test_context_for_token(client)
            return_results(microsoft_365_defender_advanced_hunting_command(client, args))

        elif command == "microsoft-365-defender-incident-get":
            test_context_for_token(client)
            return_results(microsoft_365_defender_incident_get_command(client, args))

        elif command == "fetch-incidents":
            fetch_limit = arg_to_number(fetch_limit)
            fetch_timeout = arg_to_number(fetch_timeout) if fetch_timeout else None
            incidents = fetch_incidents(client, mirroring_fields, first_fetch_time, fetch_limit, fetch_timeout)
            demisto.incidents(incidents)

        elif command == "get-remote-data":
            test_context_for_token(client)
            return_results(get_remote_data_command(client, args))

        elif command == "get-modified-remote-data":
            test_context_for_token(client)
            modified_incidents = get_modified_remote_data_command(client, args)
            return_results(modified_incidents)

        elif command == "update-remote-system":
            test_context_for_token(client)
            return_results(update_remote_system_command(client, args))

        elif command == "get-mapping-fields":
            return_results(get_mapping_fields_command())

        else:
            raise NotImplementedError
    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()