Microsoft Teams

Send messages and notifications to your team members.

Messaging and Conferencing · Microsoft Teams

Details

IDMicrosoft Teams
ProviderMicrosoft
CategoryMessaging and Conferencing
From Version5.0.0
Docker Imagedemisto/teams:1.0.0.10133006
Supported ModulesAgentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR

README

Use the Microsoft Teams integration to send messages, notifications, and create meetings.
This integration was tested with version 1.0 of Microsoft Teams.

This document includes the following sections to help you understand, set up, and use the integration effectively:

Integration Architecture

Data is exchanged between Microsoft Teams and Cortex XSOAR/XSIAM through a bot that you configure in Teams. A webhook (that you will configure) receives the data from Teams and passes it to the messaging endpoint. The web server on which the integration runs in Cortex XSOAR/XSIAM listens to the messaging endpoint and processes the data from Teams. You can use an engine for communication between Teams and the Cortex XSOAR/XSIAM tenant. To mirror messages from Teams into Cortex XSOAR, the bot must be explicitly mentioned in the message using the ‘@’ symbol.
The web server for the integration runs within a long-running Docker container. Cortex XSOAR/XSIAM maps the Docker port to which the server listens, to the host port (to which Teams posts messages). For more information, see long-running integrations documentation and Docker documentation.

Protocol Diagram

image

Setup and Configuration

Note: The information in this video is for Cortex XSOAR 6 but mostly still applies to Cortex XSOAR 8/XSIAM. Refer to the Prerequisites section for the latest instructions.

Sorry, your browser doesn't support embedded videos. You can download the video at: https://github.com/demisto/content-assets/blob/master/Assets/MicrosoftTeams/FullConfigVideo.mov?raw=true

Prerequisites

  1. Create the Demisto Bot in Microsoft Azure Portal
  2. Grant the Demisto Bot Permissions
  3. Configure Microsoft Teams on Cortex XSOAR/XSIAM
  4. Add the Demisto Bot to a Team

1. Create the Demisto Bot in Microsoft Teams

Creating the Demisto Bot using Microsoft Azure Portal:

  1. Navigate to the Create an Azure Bot page.
  2. In the Bot Handle field, type Demisto Bot.
  3. Fill in the required Subscription and Resource Group, relevant links: Subscription, Resource Groups.
  4. For Type of App, select Single Tenant.
    • Note ⚠️: The Multi Tenant App type was deprecated by Microsoft.
      Existing apps remain functional and do no require any changes.
      You can change existing apps to a Single Tenant in the Azure portal’s bot configuration, but it is not required.
  5. For Creation type, select Create new Microsoft App ID for Creation Type if you don’t already have an app registration, otherwise, select Use existing app registration, and fill in you App ID.
    • Note ⚠️: if you choose Use existing app registration, make sure to delete the previous created bot with the same app id, remove it from the team it was added to as well.
  6. Click Review + Create, and wait for the validation to pass.
  7. Click Create if the validation has passed, and wait for the deployment to finish.
  8. Under Next Steps, click Go to resource.
  9. This step can be skipped for now, return to it after configuring the Microsoft Teams instance in Cortex XSOAR/XSIAM.

    Navigate to Settings -> Configuration on the left bar, and fill in the Messaging Endpoint.

    • Note ⚠️: To get the correct messaging endpoint based on the server URL, the server version, and the instance configurations, use the microsoft-teams-create-messaging-endpointcommand.
      Using this command requires an active integration instance. This step can be done after completing the instance configuration section.
  10. Store the Microsoft App ID value for the next steps, and navigate to Manage next to it.
  11. Click New Client Secret, fill in the Description and Expires fields as desired. Then click Add.
  12. Copy the client secret from the value field and store it for the next steps.
  13. Go back to the previous page, and navigate to Channels in the left bar.
  14. Click Microsoft Teams under Available Channels, click the checkbox, click Agree, then click Apply.

2. Grant the Demisto Bot Permissions in Microsoft Graph

In order to connect to Microsoft Teams use one of the following authentication methods:

  • Client Credentials Flow
  • Authorization Code Flow (Supports the chat commands)

Perform the following steps to add the needed permissions:

  1. Go to your Microsoft Azure portal, and from the left navigation pane select Entra ID > App registrations.
  2. Search for and click Demisto Bot.
  3. Click API permissions > Add a permission > Microsoft Graph > Application/Delegated permissions.
  4. For each of the next permissions listed below, search for the permission, select the checkbox, and click Add permissions.

Note ⚠️: The microsoft-teams-ring-user command requires authenticating with Client Credentials due to a limitation in Microsoft’s permissions system. (Calling this command will perform the authentication seemlessly)

For Client Credentials Flow add the following

Application Permissions:

  • User.Read.All
  • GroupMember.Read.All
  • Channel.ReadBasic.All
  • ChannelMember.ReadWrite.All
  • Channel.Create
  • Channel.Delete.All
  • OnlineMeetings.ReadWrite.All
  • Calls.Initiate.All

For Authorization Code Flow add the following

Delegated Permissions:

  • User.Read.All
  • GroupMember.Read.All
  • Channel.ReadBasic.All
  • ChannelMember.ReadWrite.All
  • Channel.Create
  • Channel.Delete.All
  • ChannelMessage.Send
  • OnlineMeetings.ReadWrite.All
  • Chat.ReadWrite
  • AppCatalog.Read.All
  • TeamsAppInstallation.ReadWriteSelfForChat

Application permissions: (For microsoft-teams-ring-user)

  • User.Read.All
  • Calls.Initiate.All

Alternatively, you can check each relevant command section below for the minimum permissions it requires.

  1. Verify that all permissions were added, and click Grant admin consent.

  2. When prompted to verify granting permissions, click Yes, and verify that permissions were successfully added.

  3. Click Authentication > Platform configurations > Add a platform. Choose Web and add Redirect URIs: https://oproxy.demisto.ninja/authcode

3. Configure Microsoft Teams on Cortex XSOAR/XSIAM

There are two flows to configure the integration (mentioned before):

Please note that you need to use the flow to which you have added permissions.

  1. Client Credentials
  2. Authorization Code (Support The chat commands)

For more detailed instructions, refer to the relevant authentication flow section.

To configure the integration, do the following:

  1. Search for Microsoft Teams integration after you have installed it from the Marketplace.
  2. Click Add instance to create and configure a new integration instance by using one of the flows. Instructions below.
Parameter Description Required
Bot ID   False
Bot Password   False
Authentication Type When switching from the ‘Client Credentials Flow’ to the ‘Authorization Code Flow’ do as follow: Save the instance, run the ‘microsoft-teams-generate-login-url’ command and follow the instructions mentioned there. Once done, test the validity of your instance by running the ‘microsoft-teams-auth-test’ command.
When switching from the ‘Authorization Code Flow’ to the ‘Client Credentials Flow’, test the validity of the instance by clicking the ‘Test’ button.
False
Application redirect URI (for Authorization Code mode)   False
Authorization code   False
Default team   True
Notifications channel   True
Certificate (Required for HTTPS)   False
Private Key (Required for HTTPS)   False
Bot Type Converted to Single-Tenant Select this option if the bot was originally created as multi-tenant and later converted to single-tenant. The system will detect the bot type automatically in all other standard cases. False
Minimum incident severity to send notifications to Teams by   False
Disable Automatic Notifications Whether to disable automatic notifications to the configured notifications channel. False
Allow external users to create incidents via direct message   False
The header of an external form hyperlink.   False
Trust any certificate (not secure) Do not check for Cortex XSOAR version 8 False
Use system proxy settings   False
Custom New Incident Welcome Message Optional - Welcome message when creating new incidents using the bot. If left empty (default option) the current default welcome message will be used, i.e., “Successfully created incident <incident_name>. View it on: <incident_link>”. If the parameter is set to “no_welcome_message” value, no message will be displayed. Any non-empty string value set will be used as the custom new incident welcome message. False
Long running instance   False
Listen port E.g., 7000 (Required for investigation mirroring and direct messages). False
Incident type   False

Authentication Using the Client Credentials Flow

Note ⚠️: The chat commands are only supported when using the Authorization Code flow.

  1. Choose the ‘Client Credentials’ option in the Authentication Type parameter.
  2. Enter your Client/Application ID in the Bot ID parameter.
  3. Enter your Client Secret in the Bot Password parameter.
  4. Set the Default team and the Notifications channel parameters.
  5. Set the Long running instance parameter to ‘True’.
  6. Save the instance.
  7. Click Test to validate the URLs, token, and connection.
  8. Configure the messaging endpoint if you haven’t done so already (Step #9 in the Bot Creation section).
  9. Add the Demisto Bot to a Team.

Authentication Using the Authorization Code Flow

Note ⚠️: The microsoft-teams-ring-user command requires authenticating with Client Credentials.

  1. Choose the ‘Authorization Code’ option in the Authentication Type parameter.
  2. Enter your Client/Application ID in the Bot ID parameter.
  3. Enter your Client Secret in the Bot Password parameter.
  4. Enter your Application redirect URI in the Application redirect URI parameter (Step #7 in the Grant Permissions section).
  5. Set the Default team and the Notifications channel parameters.
  6. Set the Long running instance parameter to ‘True’.
  7. Save the instance.
  8. Configure the messaging endpoint if you haven’t done so already (Step #9 in the Bot Creation section).
  9. Add the Demisto Bot to a Team
  10. Run the !microsoft-teams-generate-login-url command in the Playground and follow the on-screen instructions.
  11. Save the instance.
  12. Run the !microsoft-teams-auth-test command. A ‘Success’ message should be printed to the War Room.

If you encounter any issue or error — especially a message saying “Tenant ID is not found” — please refer to the Troubleshooting section.

4. Add the Demisto Bot to a Team

According to Microsoft it usually takes up to 3-5 business days for the app to reflect in the “built for your org” section.

After configuring the integration on Cortex XSOAR/XSIAM (the previous step), do the following:

  1. Download the ZIP file located at the Download Demisto Bot section.
  2. Uncompress the ZIP file. You should see 3 files (manifest.json, color.png and outline.png).
  3. Open the manifest.json file that was extracted from the ZIP file.
  4. Update the following values to use the Bot ID from step 5 of the Create the Demisto Bot in Microsoft Teams section:

    • The id field.
    • The BotId attribute in the bots list.
    • The id attribute in the webApplicationInfo field.
  5. Compress the 3 files (the modified manifest.json file, color.png and outline.png).
  6. Navigate to Manage Apps in the Microsoft Teams admin center.
  7. Click Actions and then + Upload new app.
  8. In the pop-up window, click Upload.
  9. Browse for the ZIP file you created in step 7, open it, and wait a few seconds until it loads.
  10. Search for Demisto Bot.
  11. In the line where Demisto Bot shows under Name, click the checkbox on the left.
  12. Click Add to team.
  13. In the search box, type the name of the team to which you want to add the bot.
  14. Click Add for the wanted team and then click Apply.

Migration from Cortex XSOAR 6 to Cortex XSOAR 8 and Cortex XSIAM

Using Cortex XSOAR or Cortex XSIAM rerouting

When migrating from Cortex XSOAR 6 to Cortex XSOAR 8 or Cortex XSIAM, follow these steps to ensure your Microsoft Teams integration continues to work as expected:

  1. For this step you can use the microsoft-teams-create-messaging-endpoint command to generate the messaging endpoint automatically.
    Update the messaging endpoint in the Azure bot configuration:
    • For Cortex XSOAR 8, set the messaging endpoint in the Azure bot to be https://ext-<CORTEX-XSOAR-SERVER-ADDRESSS>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME>, e.g., https://ext-my.demisto.live/xsoar/instance/execute/teams.
    • For Cortex XSIAM, set the messaging endpoint in the Azure bot to be https://ext-<CORTEX-XSIAM-SERVER-ADDRESSS>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME>, and replace the xdr in the URL to crtx.
  2. Ensure the long-running instance parameter is enabled in the integration instance configuration.

  3. Set the port parameter under the Connect section of the integration instance.

  4. If you’re reusing the same bot from your XSOAR 6 instance, remove the bot from the Microsoft Teams team and add it back:

    1. In Microsoft Teams, go to your team.

    2. Click the three dots next to the team name.

    3. Go to Manage team > Apps.

    4. Find your bot, click the three dots next to it.

    5. Select Remove.

    6. Add the bot to the team again.

Setup Examples

1. Using Cortex XSOAR or Cortex XSIAM rerouting

In this configuration, we will use Cortex XSOAR/Cortex XSIAM functionality, which reroutes HTTPS requests that hit the default port (443) to the web server that the integration spins up.

The messaging endpoint needs to be:

For Cortex XSOAR version 6.x: <CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME>, e.g., https://my.demisto.live/instance/execute/teams.

For Cortex XSOAR version 8 and Cortex XSIAM: https://ext-<CORTEX-XSOAR-SERVER-ADDRESSS>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME>, e.g., https://ext-my.demisto.live/xsoar/instance/execute/teams.

The integration instance name, teams in this example, needs to be configured in the Configure Microsoft Teams on Cortex XSOAR/XSIAM step. Make sure to set the instance name in all lowercase letters and as one word.

  • Note ⚠️: You can use the microsoft-teams-create-messaging-endpoint command to generate the messaging endpoint, based on the server URL, the server version, and the instance configurations. For more information, see -microsoft-teams-create-messaging-endpoint documentation.

The port to be configured in Configure Microsoft Teams on Cortex XSOAR/XSIAM step should be any available port that is not used by another service.

In addition, make sure Instance execute external is enabled (for Cortex XSOAR 6.x).

  1. In Cortex XSOAR, go to Settings > About > Troubleshooting.
  2. In the Server Configuration section, verify that the instance.execute.external.<INTEGRATION-INSTANCE-NAME> (instance.execute.external.teams in this example) key is set to true. If this key does not exist, click + Add Server Configuration and add the instance.execute.external.<INTEGRATION-INSTANCE-NAME> and set the value to true. See the following reference article for further information.

2. Using NGINX as reverse proxy

In this configuration, the inbound connection, from Microsoft Teams to Cortex XSOAR/Cortex XSIAM, goes through a reverse proxy (e.g., NGINX) which relays the HTTPS requests posted from Microsoft Teams
to the Cortex XSOAR/Cortex XSIAM server on HTTP.

On NGINX, configure the following:

  • SSL certificate under ssl_certificate and ssl_certificate_key
  • The Cortex XSOAR server (including the port) under proxy_pass, e.g. http://mydemistoinstance.com:7000

Follow Configuring Upstream Servers NGINX guide for more details.

The port (7000 in this example), to which the reverse proxy should forward the traffic on HTTP, should be the same port you specify in the integration instance configuration, as the web server the integration spins up, listens on that port.

image

image

3. Using Apache reverse proxy and Cortex XSOAR/XSIAM engine

In this configuration, the inbound connection, from Microsoft Teams to Cortex XSOAR/XSIAM, goes through a reverse proxy (e.g., Apache) and possibly a load balancer, which relays the HTTPS requests posted from Microsoft Teams
to a Cortex XSOAR/XSIAM engine, which can be put in a DMZ, on HTTP.

The port (7000 in this example), to which the reverse proxy should forward the traffic on HTTP, should be the same port you specify in the integration instance configuration, as the web server the integration spins up, listens on that port.

image

image

4. Using Cloudflare

In this configuration, we will use Cloudflare proxy.

The messaging endpoint should be the Cortex XSOAR/XSIAM URL, which needs to be hosted on Cloudflare, with the port to which Cloudflare proxy directs the HTTPS traffic, e.g., https://mysite.com:8443

In the Configure Microsoft Teams on Cortex XSOAR/XSIAM step, the following need to be configured:

  • The port selected above.
  • A certificate and key for configuring HTTPS web server. This certificate can be self-signed.

The proxy intercepts HTTPS traffic, presents a public CA certificate, then proxies it to the web server.

All HTTPS traffic that will hit the selected messaging endpoint will be directed to the HTTPS web server the integration spins up, and will then be processed.

Important Information

  • This integration is supported in Cortex XSOAR 8 and up and Cortex XSIAM without using an engine.
  • The integration has the ability to run built-in Cortex XSOAR/XSIAM commands, through a mirrored channel. Make sure to pass the command in the chat exactly as typed in the CORTEX XSOAR CLI. For example: !DeleteContext all=yes. Use the command mirror-investigation to mirror/create a mirrored channel.
  • For use cases where it is only needed to send messages to a specific channel, we recommend checking the Microsoft Teams via Webhook Integration, which has a simpler setup.
  • The messaging endpoint must be one of the following:
    • The URL of the Cortex XSOAR/XSIAM server, including the configured port
    • The Cortex XSOAR/XSIAM rerouting URL that you’ve defined for your Microsoft Teams instance (see the Using Cortex XSOAR or Cortex XSIAM rerouting section for more details)
    • A proxy that redirects the messages received from Teams to the Cortex XSOAR/XSIAM server (see the Using NGINX as reverse proxy section for more details)
  • Microsoft Teams sends events to the messaging endpoints via an HTTPS request, so the messaging endpoint must be accessible for Microsoft Teams to reach it. As a result, the messaging endpoint cannot contain a private IP address or any DNS that will block the request from Microsoft Teams.
  • To verify that the messaging endpoint is open as expected, from a browser, go to the messaging endpoint in an environment that is disconnected from the Cortex XSOAR/XSIAM environment.
  • It’s important that the port is opened for outside communication and that the port is not being used, meaning that no service is listening on it. Therefore, the default port, 443, should not be used.
  • For additional security, we recommend placing the Teams integration web server behind a reverse proxy (such as NGINX).
  • By default, the web server that the integration starts provides services in HTTP. For communication to be in HTTPS you need to provide a certificate and private key in the following format:

       -----BEGIN PRIVATE KEY-----
       ...
       -----END PRIVATE KEY-----
    
  • You must not set a certificate and/or private key if you are using the Cortex XSOAR/XSIAM rerouting setup.
  • Microsoft does not support self-signed certificates and requires a chain-trusted certificate issued by a trusted CA.

In order to verify which certificate is used, run the following (replace {MESSAGING-ENDPOINT} with the messaging endpoint):

 curl {MESSAGING-ENDPOINT} -vI

Make sure the output does not contain the following:

 curl: (60) SSL certificate problem: self signed certificate
  • The following domains are used by this integration:

Known Limitations

  • The microsoft-teams-ring-user command requires using the Client Credentials authentication due to a limitation in Microsoft’s permissions system. As such, when using Authorization Code flow and calling this command, the integration will internally authenticate using the Client Credentials flow.
  • The chat commands are only supported when using the Authorization Code flow.
  • Posting a message or adaptive card to a private/shared channel is currently not supported in the send-notification command. Thus, also the mirror_investigation command does not support private/shared channels. For more information, see Microsoft General known issues and limitations.
  • The send-notification command currently supports only adaptive cards without user responses. To collect user responses from adaptive cards, please use the MicrosoftTeamsAsk command.
  • In case of multiple chats/users sharing the same name, only one will be used.
  • If a non-Cortex XSOAR/XSIAM user ran the new incident command in the chat with the bot, the owner of the created incident would be the logged in Cortex XSOAR/XSIAM user, not the external user who ran the command.
  • See Microsoft documentation for Limits and specifications for Microsoft Teams.

Troubleshooting

  1. The integration works by spinning up a web server that listens to events and data posted to it from Microsoft Teams.
    If you see the error message Tenant ID is missing, please make sure that the messaging endpoint is configured correctly, and the bot is added to a team., then it means that the tenant ID was never posted to the web server, which should happen for the first time when the bot is added to the configured team.

    This probably means that there is a connection issue, and the web server does not intercept the HTTPS queries from Microsoft Teams.

    To troubleshoot:

    1. Verify that the messaging endpoint is configured correctly. Use the microsoft-teams-create-messaging-endpointcommand (microsoft-teams-create-messaging-endpoint documentation) to get the correct messaging endpoint based on the server URL, the server version, and the instance configurations.
    2. In some cases, a connection is not created between Teams and the messaging endpoint when adding a bot to the team. You can work around this problem by adding any member to the team the bot was added to (the bot should be already added to the team). This will trigger a connection and solve the issue. You can then remove the member that was added.
    3. If the previous step did not work, remove the bot from the team, go to the Microsoft Teams admin center > Manage apps and hard refresh the page!(cmd+ shift + R), then add the bot to the team again.
    4. The integration stores in cache metadata about the teams, members and channels. Starting from Cortex XSOAR version 6.1.0, you can clear the integration cache in the integration instance config:

      First, make sure to remove the bot from the team (only via the Teams app), before clearing the integration cache, and add it back after done.
      If the bot belongs to multiple teams, make sure to remove it from all the teams it was added to, and then clear the cache.

    5. Verify the Docker container is up and running and publish the configured port to the outside world:

      From the Cortex XSOAR/XSIAM engine machine run:

      docker ps | grep teams

      You should see the following, assuming port 7000 is used:

      988fdf341127 demisto/teams:1.0.0.6483 "python /tmp/pyrunne…" 6 seconds ago Up 4 seconds 0.0.0.0:7000->7000/tcp demistoserver_pyexecLongRunning-b60c04f9-754e-4b68-87ed-8f8113419fdb-demistoteams1.0.0.6483--26

      If the Docker container is up and running, try running cURL queries to verify the web server is up and running and listens on the configured URL:

      • To the messaging endpoint from a separate box.
      • From the Cortex XSOAR machine to localhost.
        • Note ⚠️: The web server supports only POST method queries.

      If the cURL queries were sent successfully, you should see the following line in Cortex XSOAR logs: Finished processing Microsoft Teams activity successfully.

    6. If you’re working with secured communication (HTTPS), make sure that you provided a valid certificate. (Not for Cortex XSOAR/Cortex XSIAM Rerouting ).
      1. Run openssl s_client -connect <domain.com>:443 .
      2. Verify that the returned value of the Verify return code field is 0 (ok), otherwise, it’s not a valid certificate.
  2. If you see the following error message: Error in API call to Microsoft Teams: [403] - UnknownError, it means the AAD application has insufficient permissions.

    • To retrieve the API permission associated with the used graph access token you can run the microsoft-teams-token-permissions-list command (microsoft-teams-token-permissions-list documentation).

    • Compare the permissions list obtained for the token with the permissions required for the command you wish to execute (can be found in the command documentation).

    • If there are missing API permissions, add them to your application, and then run the microsoft-teams-auth-reset command (as described here - microsoft-teams-auth-reset documentation).

    • If your authentication type is the Authorization Code Flow, after running the microsoft-teams-auth-reset command you will need to regenerate the Authorization code parameter by running the microsoft-teams-generate-login-url command, and to verify the authentication by running the !microsoft-teams-auth-test command.

  3. Since the integration works based on Docker port mapping, it can’t function if the Docker is set to run with the host networking (--network=host). For more details, refer to the Docker documentation.
  4. If you are receiving repeated Connection reset by peer errors, the requests might be getting blocked temporarily by Azure due to repeated permission errors. Ensure you are not missing any permissions that might cause constant failures and eventually leading to server timeouts.
  5. If you encounter the following error:

     Error code [401] in API call to Microsoft Teams:
     Authorization error in call to the Microsoft Bot Framework, ensure the "Microsoft Teams" channel was added to the bot in the Azure portal during setup.
    
     (Error Message): {"message":"Authorization has been denied for this request."}
    

    And the bot’s application type was converted from Multi-tenant to Single-tenant, the integration may fail to recognize the change, leading to the denial of authorization.

    To resolve this:

    • Navigate to your instance configuration settings.

    • Select the Bot Type Converted to Single-Tenant parameter in the advanced settings of the Connect section.

    • Run the command !microsoft-teams-auth-reset in the War Room.

    Your bot should now be properly authorized and functional.

  6. If you see the following error message: Forbidden: Failed to get license information for the user. Ensure user has a valid Office365 license assigned to them., it indicates that the user who approved the authentication URL likely does not have a valid Office 365 license assigned.

    To resolve this, please follow these steps:

    • Re-authorize: Re-run the command !microsoft-teams-generate-login-url and open the URL that is returned. This time, approve the authorization using a different user who has a valid Office 365 license.

    • Update Instance: Copy the new Authorization Code that is generated and paste it into the Authorization code parameter in the instance settings.

    • Reset Token: Run the command !microsoft-teams-auth-reset in the Playground.

Download Demisto Bot

Demisto Bot zip

Running commands from Microsoft Teams

You can run Cortex XSOAR/XSIAM commands, according to the user permissions, from Microsoft Teams in a mirrored investigation channel.
For Microsoft Teams integration commands to be sent to Cortex XSOAR/XSIAM, you need to have role/access to Cortex XSOAR/XSIAM and your Teams email needs to match the email/user in Cortex XSOAR/XSIAM. External users can create incidents if the flag is set in the integration, but the rest of the commands require a valid Cortex XSOAR/XSIAM user and role.

Direct messages commands

You can chat with the bot in direct messages in order to retrieve data (list incidents and tasks) and run operations (create incident and mirror an investigation) related to Cortex XSOAR.

You can send the message help in order to see the supported commands:

image

Note ⚠️: To enrich an incident created via the Demisto BOT (new incident command) with extra information received with the request, as in regular fetch-incidents process users may create custom mappers and map the desired values.

Commands

You can execute these commands from the Cortex XSOAR/XSIAM 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.

send-notification


Sends a message to the specified teams.
To mention a user in the message, add a semicolon “;” at the end of the user mention. For example: @Bruce Willis;.

If sending a reply to a message, the message ID must be provided and the reply will be sent via the Graph API which means
the message will appear from the account used to authorize the integration instance and not the bot. Setting the account’s name
and picture to match the bot will make it appear to be from the same source.

Base Command

send-notification

Required Permissions

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

ChannelMessage.Send - Delegated (Authorization Code) - Only needed for sending replies to messages

Input
Argument Name Description Required
channel The channel to which to send messages. Supports only standard channels. Optional
message The message to send to the channel or team member. Optional
team_member Display name or email address of the team member to send the message to. Optional
team The team in which the specified channel exists. The team must already exist, and this value will override the default channel configured in the integration parameters. Optional
message_id ID of the message to send the notification to as a reply when sending to a channel. Optional
adaptive_card The Microsoft Teams adaptive card to send. Optional
to The team member to which to send the message. Optional
external_form_url_header The header of an external form hyperlink. Default is Microsoft Teams Form. Optional
Context Output
Path Type Description
MicrosoftTeams.Message.ID String ID of the message sent.
Command Example

!send-notification channel=General message="hello world!" team=DemistoTeam

Human Readable Output

Message was sent successfully.

mirror-investigation


Mirrors the Cortex XSOAR/Cortex XSIAM investigation to the specified Microsoft Teams channel. Supports only standard channels.

Note: Mirrored channels could be used to run Cortex XSOAR/Cortex XSIAM built-in commands.

Base Command

mirror-investigation

Required Permissions

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.Create - Application (Client Credentials) / Delegated (Authorization Code)

Channel.Delete.All - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
mirror_type The mirroring type. Can be “all”, which mirrors everything, “chat”, which mirrors only chats (not commands), or “none”, which stops all mirroring. Possible values are: all, chat, none. Default is all. Optional
autoclose Whether to auto-close the channel when the incident is closed in Cortex XSOAR. If “true”, the channel will be auto-closed. Possible values are: true, false. Default is true. Optional
direction The mirroring direction. Possible values are: Both, FromDemisto, ToDemisto. Default is both. Optional
team The team in which to mirror the Cortex XSOAR investigation. If not specified, the default team configured in the integration parameters will be used. Optional
channel_name The name of the channel. The default is “incident-INCIDENTID”. Optional
Context Output

There is no context output for this command.

Command Example

!mirror-investigation mirror_type=all autoclose=true direction=Both

Human Readable Output

Investigation mirrored successfully in channel incident-100.

close-channel


Deletes the specified Microsoft Teams channel.

Base Command

close-channel

Required Permissions

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.Delete.All - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
channel The name of the channel to close. Optional
team The channel’s team. Optional
Context Output

There is no context output for this command.

Command Example

!close-channel channel="example channel"

Human Readable Output

Channel was successfully closed.

microsoft-teams-integration-health


Returns real-time and historical data on the integration status.

Base Command

microsoft-teams-integration-health

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-teams-integration-health

Human Readable Output

Microsoft API Health

| Bot Framework API Health | Graph API Health |
|————————–|——————|
| Operational | Operational |
No mirrored channels.

microsoft-teams-ring-user


Rings a user’s Teams account. Note: This is a ring only! no media will play in case the generated call is answered.

Base Command

microsoft-teams-ring-user

Required Permissions

User.Read.All - Application

Calls.Initiate.All - Application

Input
Argument Name Description Required
username The display name of the member to call. Required
Context Output

There is no context output for this command.

Command Example

!microsoft-teams-ring-user username="Avishai Brandeis"

Human Readable Output

Calling Avishai Brandeis

microsoft-teams-add-user-to-channel


Adds a member (user) to a private/shared channel.
For a comparison of Teams features for each channel type, see the Microsoft documentation: Channel feature comparison.

Base Command

microsoft-teams-add-user-to-channel

Required Permissions

User.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

ChannelMember.ReadWrite.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
channel The channel to which to add the member. Required
team The channel’s team. Required
member The display name of the member to add to the channel. Required
owner Whether to add the member with the owner role. Default is ‘false’ Optional
Context Output

There is no context output for this command.

Command Example

!microsoft-teams-add-user-to-channel channel="example channel" member=itayadmin team=DemistoTeam

Human Readable Output

The User “itayadmin” has been added to channel “example channel” successfully.

microsoft-teams-create-channel


Creates a new channel in a Microsoft Teams team.
For more information about the channels types, see the Microsoft documentation: standard, private, or shared channels
See also Channel feature comparison.

Base Command

microsoft-teams-create-channel

Required Permissions

User.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.Create - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
channel_name The name of the channel. Required
description The description of the channel. Optional
team The team in which to create the channel. Required
membership_type The type of the channel. Possible values are: private, standard, shared. Default is standard. Optional
owner_user The channel owner (Display name/mail/UPN) Optional
Context Output

There is no context output for this command.

Command Example

!microsoft-teams-create-channel channel_name="example channel" team=DemistoTeam description="this is my new channel"

Human Readable Output

The channel “example channel” was created successfully

microsoft-teams-create-meeting


Creates a new meeting in Microsoft Teams.

Base Command

microsoft-teams-create-meeting

Required Permissions

OnlineMeetings.ReadWrite.All - Application (Client Credentials)

OnlineMeetings.ReadWrite - Delegated (Authorization Code)

When using Client Credentials Flow:
Besides setting up this permission, in order to create a meeting, the Azure admin needs to configure application access policy
and grant users permissions to create meetings.
The script ConfigureAzureApplicationAccessPolicy was created to support the needed commands.
For more information:
Allow applications to access online meetings on behalf of a user

When using Authorization Code Flow:
The authentication process is conducted on behalf of the specific user who initiated the login request. Therefore, the given member must be the same user.

Input
Argument Name Description Required
start_time The meeting start time in ISO 8601 format e.g., “2019-07-12T14:30:34.2444915-07:00”. Optional
end_time The meeting end time in ISO 8601 format e.g., “2019-07-12T14:30:34.2444915-07:00”. Optional
subject The meeting subject. Required
member Display name/mail/UPN of user who created the meeting, e.g., Adam Testman. Required
Context Output
Path Type Description
MicrosoftTeams.CreateMeeting.creationDateTime Date Meeting creation time.
MicrosoftTeams.CreateMeeting.threadId String Meeting thread ID.
MicrosoftTeams.CreateMeeting.messageId String Meeting message ID.
MicrosoftTeams.CreateMeeting.id String Meeting ID.
MicrosoftTeams.CreateMeeting.joinWebUrl String The URL to join the meeting.
MicrosoftTeams.CreateMeeting.participantId String The meeting participants.
MicrosoftTeams.CreateMeeting.participantDisplayName String The display name of the participants.
Command Example

!microsoft-teams-create-meeting member="example user" subject="Important meeting"

Human Readable Output

The meeting “Important meeting” was created successfully

microsoft-teams-user-remove-from-channel


Removes a member (user) from a private/shared channel.

Base Command

microsoft-teams-user-remove-from-channel

Required Permissions

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

ChannelMember.ReadWrite.All - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
channel_name The name of the channel. Required
team The name of the channel’s team. Required
member The display name of the member to remove from the channel. Required
Context Output

There is no context output for this command.

Command Example

!microsoft-teams-user-remove-from-channel channel_name="example channel" member=itayadmin team=DemistoTeam

Human Readable Output

The User “itayadmin” has been removed from channel “example channel” successfully.

microsoft-teams-channel-user-list


Retrieves a list of members from a channel.

Base Command

microsoft-teams-channel-user-list

Required Permissions

GroupMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Channel.ReadBasic.All - Application (Client Credentials) / Delegated (Authorization Code)

ChannelMember.Read.All - Application (Client Credentials) / Delegated (Authorization Code)

Input
Argument Name Description Required
channel_name The name of the channel. Required
team The name of the channel’s team. Required
Context Output
Path Type Description
MicrosoftTeams.ChannelList.channelId String The channel ID.
MicrosoftTeams.ChannelList.channelName String The name of the channel.
MicrosoftTeams.ChannelList.members.displayName String The display name of the members.
MicrosoftTeams.ChannelList.members.email String The email of the members.
MicrosoftTeams.ChannelList.members.id String The ID of the members.
MicrosoftTeams.ChannelList.members.roles String The roles of the members.
MicrosoftTeams.ChannelList.members.tenantId String The tenant ID of the members.
MicrosoftTeams.ChannelList.members.userId String The user ID of the members.
MicrosoftTeams.ChannelList.members.visibleHistoryStartDateTime String The timestamp denoting how far back a conversation’s history is shared with the conversation member.
Command Example

!microsoft-teams-channel-user-list channel_name="example channel" team=DemistoTeam

Human Readable Output
Channel ‘example channel’ Members List
User Id Email Tenant Id Membership id User roles Display Name Start DateTime
359d2c3c-162b-414c-b2eq-386461e5l050 test@gmail.com pbae9ao6-01ql-249o-5me3-4738p3e1m941 MmFiOWM3OTYtMjkwMi00NWY4LWI3MTItN2M1YTYzY2Y0MWM0IyNlZWY5Y2IzNi0wNmRlLTQ2OWItODdjZC03MGY0Y2JlMzJkMTQ= owner itayadmin 0001-01-01T00:00:00Z

Chat Commands

microsoft-teams-chat-create


Creates a new chat.

Notes:

  • Only one oneOnOne chat can exist between two members. If a oneOnOne chat already exists, it will be returned.
  • This command works with the consent user, not with the bot. Which means, the chat is created between the consent user and the user provided in the command’s argument.
  • This command may fail if the bot app has not yet appeared in the “built for your org” section in teams.
Base Command

microsoft-teams-chat-create

Required Permissions

User.Read.All - Delegated

Chat.Create - Delegated

AppCatalog.Read.All - Delegated

TeamsAppInstallation.ReadWriteSelfForChat - Delegated

Input
Argument Name Description Required
chat_type Specifies the type of chat. Possible values are: group, oneOnOne. Default is group. Required
member Display name/mail/UPN of user that should be added to the chat. Can be an array. Optional
chat_name The title of the chat. The chat title can be provided only if the chat is of group type. Optional
Context Output
Path Type Description
MicrosoftTeams.ChatList.chatId String The chat’s unique identifier.
MicrosoftTeams.ChatList.topic String Subject or topic for the chat. Only available for group chats.
MicrosoftTeams.ChatList.createdDateTime String Date and time at which the chat was created.
MicrosoftTeams.ChatList.lastUpdatedDateTime String Date and time at which the chat was renamed or list of members were last changed.
MicrosoftTeams.ChatList.chatType String Specifies the type of chat.
MicrosoftTeams.ChatList.webUrl String The URL for the chat in Microsoft Teams. The URL should be treated as an opaque blob, and not parsed.
MicrosoftTeams.ChatList.tenantId String The identifier of the tenant in which the chat was created.
MicrosoftTeams.ChatList.viewpoint String Represents caller-specific information about the chat, such as last message read date and time.
MicrosoftTeams.ChatList.onlineMeetingInfo String Represents details about an online meeting. If the chat isn’t associated with an online meeting, the property is empty.
Command Example

!microsoft-teams-chat-create chat_type=group member="itayadmin, Bruce Willis" chat_name="example chat"

Human Readable Output
The chat ‘example chat’ was created successfully
Chat Id Chat name Created Date Time Last Updated Date Time webUrl Tenant Id
19:2da4c29f6d7041eca70b638b43d45437@thread.v2 example chat 2023-01-08T07:51:53.07Z 2023-01-08T07:51:53.07Z webUrl pbae9ao6-01ql-249o-5me3-4738p3e1m941

microsoft-teams-message-send-to-chat


Sends a new chat message in the specified chat.

Notes:

  • This command works with the consent user, not with the bot. Which means, the message is sent to the given chat by the consent user, not the bot.
  • This command will fail if the consent user is not a member of the destination chat.
  • This command may fail if the bot app has not yet appeared in the “built for your org” section in teams.
  • This command can only send messages from type ‘message’
Base Command

microsoft-teams-message-send-to-chat

Required Permissions

User.Read.All - Delegated

Chat.ReadBasic - Delegated

Chat.Create - Delegated

ChatMessage.Send - Delegated

AppCatalog.Read.All - Delegated

TeamsAppInstallation.ReadWriteSelfForChat - Delegated

Note: Chat.Create is needed only when sending to one-on-one chats.

Input
Argument Name Description Required
chat The chat ID / group chat name (topic) / oneOnOne member (Display name/mail/UPN). Note - the consent user must be a member of the chat. Required
content The content of the chat message. Required
content_type The message content type. Possible values are: text, html. Default is text. Optional

Context Output

Path Type Description
MicrosoftTeams.ChatList.chatId String The chat’s unique identifier.
MicrosoftTeams.ChatList.messages.id String Unique ID of the message.
MicrosoftTeams.ChatList.messages.replyToId String ID of the parent chat message or root chat message of the thread.
MicrosoftTeams.ChatList.messages.etag String Version number of the chat message.
MicrosoftTeams.ChatList.messages.messageType String The type of chat message.
MicrosoftTeams.ChatList.messages.createdDateTime String Timestamp of when the chat message was created.
MicrosoftTeams.ChatList.messages.lastModifiedDateTime String Timestamp when the chat message is created (initial setting) or modified, including when a reaction is added or removed.
MicrosoftTeams.ChatList.messages.lastEditedDateTime String Timestamp when edits to the chat message were made. Triggers an “Edited” flag in the Teams UI. If no edits are made the value is null.
MicrosoftTeams.ChatList.messages.deletedDateTime String Timestamp at which the chat message was deleted, or null if not deleted.
MicrosoftTeams.ChatList.messages.subject String The subject of the chat message, in plaintext.
MicrosoftTeams.ChatList.messages.summary String Summary text of the chat message that could be used for push notifications and summary views or fall back views.
MicrosoftTeams.ChatList.messages.chatId String If the message was sent in a chat, represents the identity of the chat.
MicrosoftTeams.ChatList.messages.importance String The importance of the chat message.
MicrosoftTeams.ChatList.messages.locale String Locale of the chat message set by the client.
MicrosoftTeams.ChatList.messages.webUrl String Link to the message in Microsoft Teams.
MicrosoftTeams.ChatList.messages.channelIdentity String If the message was sent in a channel, represents identity of the channel.
MicrosoftTeams.ChatList.messages.policyViolation String Defines the properties of a policy violation set by a data loss prevention (DLP) application.
MicrosoftTeams.ChatList.messages.eventDetail String If present, represents details of an event that happened in a chat, a channel, or a team, for example, adding new members.
MicrosoftTeams.ChatList.messages.from String Details of the sender of the chat message.
MicrosoftTeams.ChatList.messages.body String Plaintext/HTML representation of the content of the chat message. Representation is specified by the contentType inside the body.
MicrosoftTeams.ChatList.messages.attachments String References to attached objects like files, tabs, meetings etc.
MicrosoftTeams.ChatList.messages.mentions String List of entities mentioned in the chat message.
MicrosoftTeams.ChatList.messages.reactions String Reactions for this chat message (for example, Like).
Command Example

!microsoft-teams-message-send-to-chat chat="example chat" content="Hello World"

Human Readable Output

Message was sent successfully in the ‘example chat’ chat

Chat Id Created DateTime Etag From user From user id From user userIdentityType Importance Message Content Message Type Message contentType Message id lastModified DateTime
19:2da4c29f6d7041eca70b638b43d45437@thread.v2 2021-03-29T04:17:43.15Z 1616991463150 itayadmin 8ea0e38b-efb3-4757-924a-5f94061cf8c2 aadUser normal Hello World message text 1616991463150 2021-03-29T04:17:43.15Z

microsoft-teams-chat-add-user


Adds a member (user) to a group chat.

Notes:

  • This command works with the consent user, not with the bot. Which means, the member will be added to the given chat by the consent user, not the bot.
  • This command will fail if the consent user is not a member of the destination chat.

Base Command

microsoft-teams-chat-add-user

Required Permissions

Chat.ReadBasic - Delegated

ChatMember.ReadWrite - Delegated

Input

Argument Name Description Required
chat The chat ID or group chat name (topic) to which to add the member. Note - the consent user must be a member of the chat. Required
member Display name/mail/UPN of user that should be added to the chat. Can be an array. Required
share_history Whether to share the whole history of the chat. Possible values are: true, false. Default is True. Optional

Context Output

There is no context output for this command.

Command Example

!microsoft-teams-chat-add-user chat="example chat" member="Bruce Willis" share_history=false

Human Readable Output

The User “Bruce Willis” has been added to chat “example chat” successfully.

microsoft-teams-chat-member-list


Retrieves a list of members from a chat.

Notes:

  • This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

Base Command

microsoft-teams-chat-member-list

Required Permissions

User.Read.All - Delegated

Chat.ReadBasic - Delegated

Input

Argument Name Description Required
chat The chat ID / group chat name (topic) / oneOnOne Member (Display name/mail/UPN). Note - the consent user must be a member of the chat. Required

Context Output

Path Type Description
MicrosoftTeams.ChatList.chatId String The chat’s unique identifier.
MicrosoftTeams.ChatList.members.displayName String The display name of the members.
MicrosoftTeams.ChatList.members.email String The email of the members.
MicrosoftTeams.ChatList.members.id String The ID of the members.
MicrosoftTeams.ChatList.members.roles String The roles of the members.
MicrosoftTeams.ChatList.members.tenantId String The tenant ID of the members.
MicrosoftTeams.ChatList.members.userId String The user ID of the members.
MicrosoftTeams.ChatList.members.visibleHistoryStartDateTime String The timestamp denoting how far back a conversation’s history is shared with the conversation member.
Command Example

!microsoft-teams-chat-member-list chat="example chat"

Human Readable Output

Chat “example chat” Members List

User Id User roles Name Email Tenant Id
359d2c3c-162b-414c-b2eq-386461e5l050 owner itayadmin test@gmail.com dcd219dd-bc68-4b9b-bf0b-4a33a796be35
48d31887-5fad-4d73-a9f5-3c356e68a038 owner Bruce Willis test@gmail.com dcd219dd-bc68-4b9b-bf0b-4a33a796be35

microsoft-teams-chat-list


Retrieves a list of chats that the consent user is a member of. If ‘chat’ is specified - retrieves this chat only.

Base Command

microsoft-teams-chat-list

Required Permissions

User.Read.All - Delegated

Chat.ReadBasic - Delegated

Input

Argument Name Description Required
chat The chat ID / group chat name (topic) / oneOnOne member (Display name/mail/UPN). Note - the consent user must be a member of the chat. Optional
filter Filters results. For example: topic eq ‘testing’. For more query examples, see https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http. Optional
expand Expands the results to include members or lastMessagePreview properties. Possible values are: members, lastMessagePreview. Optional
limit The number of results to retrieve. Default is 50. Optional
next_link A link that specifies a starting point to use for subsequent calls. Optional
page_size Number of results to return per page. Default is 50. Optional

Context Output

Path Type Description
MicrosoftTeams.ChatList.chatId String The chat’s unique identifier.
MicrosoftTeams.ChatList.topic String Subject or topic for the chat. Only available for group chats.
MicrosoftTeams.ChatList.createdDateTime String Date and time at which the chat was created.
MicrosoftTeams.ChatList.lastUpdatedDateTime String Date and time at which the chat was renamed or list of members were last changed.
MicrosoftTeams.ChatList.chatType String Specifies the type of chat.
MicrosoftTeams.ChatList.webUrl String The URL for the chat in Microsoft Teams. The URL should be treated as an opaque blob, and not parsed.
MicrosoftTeams.ChatList.tenantId String The identifier of the tenant in which the chat was created.
MicrosoftTeams.ChatList.viewpoint String Represents caller-specific information about the chat, such as last message read date and time.
MicrosoftTeams.ChatList.onlineMeetingInfo String Represents details about an online meeting. If the chat isn’t associated with an online meeting, the property is empty.
MicrosoftTeams.ChatListNextLink String Used if an operation returns partial results. If a response contains a NextLink element, its value specifies a starting point to use for subsequent calls.
Command Example

!microsoft-teams-chat-list filter="topic eq 'testing'"

Human Readable Output

Chats List

Chat Id Chat name Created Date Time Last Updated Date Time Chat Type webUrl Tenant Id Last Message Read Date Time
19:561082c0f3f847a58069deb8eb300807@thread.v2 testing 2023-01-08T14:15:45.412Z 2023-01-08T14:15:45.412Z group webUrl tenantId 2023-01-08T14:16:48.662Z
19:2da4c29f6d7041eca70b638b43d45437@thread.v2 testing 2022-12-29T11:10:49.173Z 2022-12-29T11:10:49.173Z group webUrl tenantId 2022-12-29T12:00:07.317Z

microsoft-teams-chat-message-list


Retrieves a list of messages in a chat.

Notes:

  • This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

Base Command

microsoft-teams-chat-message-list

Required Permissions

User.Read.All - Delegated

Chat.Read - Delegated

Input

Argument Name Description Required
chat The chat ID / group chat name (topic) / oneOnOne member (Display name/mail/UPN). Note - the consent user must be a member of the chat. Required
limit The number of results to retrieve. Default is 50. Optional
order_by Orders results by lastModifiedDateTime (default) or createdDateTime in descending order. Possible values are: lastModifiedDateTime, createdDateTime. Default is lastModifiedDateTime. Optional
next_link A link that specifies a starting point to use for subsequent calls. Optional
page_size Number of results to return per page. Default is 50. Optional

Context Output

Path Type Description
MicrosoftTeams.ChatList.chatId String The chat’s unique identifier.
MicrosoftTeams.ChatList.messages.id String Unique ID of the message.
MicrosoftTeams.ChatList.messages.replyToId String ID of the parent chat message or root chat message of the thread.
MicrosoftTeams.ChatList.messages.etag String Version number of the chat message.
MicrosoftTeams.ChatList.messages.messageType String The type of chat message.
MicrosoftTeams.ChatList.messages.createdDateTime String Timestamp of when the chat message was created.
MicrosoftTeams.ChatList.messages.lastModifiedDateTime String Timestamp when the chat message is created (initial setting) or modified, including when a reaction is added or removed.
MicrosoftTeams.ChatList.messages.lastEditedDateTime String Timestamp when edits to the chat message were made. Triggers an “Edited” flag in the Teams UI. If no edits are made the value is null.
MicrosoftTeams.ChatList.messages.deletedDateTime String Timestamp at which the chat message was deleted, or null if not deleted.
MicrosoftTeams.ChatList.messages.subject String The subject of the chat message, in plaintext.
MicrosoftTeams.ChatList.messages.summary String Summary text of the chat message that could be used for push notifications and summary views or fall back views.
MicrosoftTeams.ChatList.messages.chatId String If the message was sent in a chat, represents the identity of the chat.
MicrosoftTeams.ChatList.messages.importance String The importance of the chat message.
MicrosoftTeams.ChatList.messages.locale String Locale of the chat message set by the client.
MicrosoftTeams.ChatList.messages.webUrl String Link to the message in Microsoft Teams.
MicrosoftTeams.ChatList.messages.channelIdentity String If the message was sent in a channel, represents identity of the channel.
MicrosoftTeams.ChatList.messages.policyViolation String Defines the properties of a policy violation set by a data loss prevention (DLP) application.
MicrosoftTeams.ChatList.messages.eventDetail String If present, represents details of an event that happened in a chat, a channel, or a team, for example, adding new members.
MicrosoftTeams.ChatList.messages.from String Details of the sender of the chat message.
MicrosoftTeams.ChatList.messages.body String Plaintext/HTML representation of the content of the chat message. Representation is specified by the contentType inside the body.
MicrosoftTeams.ChatList.messages.attachments String References to attached objects like files, tabs, meetings etc.
MicrosoftTeams.ChatList.messages.mentions String List of entities mentioned in the chat message.
MicrosoftTeams.ChatList.messages.reactions String Reactions for this chat message (for example, Like).
MicrosoftTeams.MessageListNextLink String Used if an operation returns partial results. If a response contains a NextLink element, its value specifies a starting point to use for subsequent calls.
Command Example

!!microsoft-teams-chat-message-list chat="example chat" order_by=createdDateTime

Human Readable Output

Messages list in “example chat” chat

Chat Id Created DateTime Etag From user From user id From user userIdentityType Importance Message Content Message Type Message contentType Message id lastModified DateTime
19:2da4c29f6d7041eca70b638b43d45437@thread.v2 2021-03-29T04:17:43.15Z 1616991463150 itayadmin 8ea0e38b-efb3-4757-924a-5f94061cf8c2 aadUser normal Hello World message text 1616991463150 2021-03-29T04:17:43.15Z

microsoft-teams-chat-update


Updates the chat name. It can only be set for group chats.

Notes:

  • This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

Base Command

microsoft-teams-chat-update

Required Permissions

User.Read.All - Delegated

Chat.ReadWrite - Delegated

Input

Argument Name Description Required
chat The chat ID / group chat name (topic). Note - the consent user must be a member of the chat. Required
chat_name The new chat name. Maximum length is 250 characters. Use of ‘:’ is not allowed. Required

Context Output

There is no context output for this command.

Command Example

!microsoft-teams-chat-update chat="example chat" chat_name="update chat_name"

Human Readable Output

The name of chat ‘example chat’ has been successfully changed to ‘update chat_name’.

microsoft-teams-auth-test


Tests the connectivity to MicrosoftTeams.

Base Command

microsoft-teams-auth-test

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-teams-auth-test

Human Readable Output

✅ Success!

microsoft-teams-generate-login-url


Generate the login url used for Authorization code flow.
Note: Authorization codes are short-lived. Typically, they expire after about 10 minutes.

Base Command

microsoft-teams-generate-login-url

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command example

!microsoft-teams-generate-login-url

Human Readable Output

Authorization instructions

  1. Click on the login URL to sign in and grant Cortex XSOAR permissions for your Azure Service Management.
    You will be automatically redirected to a link with the following structure:
    REDIRECT_URI?code=AUTH_CODE&session_state=SESSION_STATE
  2. Copy the AUTH_CODE (without the code= prefix, and the session_state parameter)
    and paste it in your instance configuration under the Authorization code parameter.

microsoft-teams-auth-reset


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

  • After making changes to permissions in the Azure Portal, reset the authentication to ensure that the token reflects the updated permissions.
  • This command is triggered automatically when an authentication flow type switch is detected. The auto resetting ensures the integration uses the appropriate token.
  • When switching the authentication type to the Authorization Code Flow, this command will be triggered automatically. Then you will need to regenerate the Authorization code parameter by running the microsoft-teams-generate-login-url command, and to verify the authentication by running the !microsoft-teams-auth-test command.

Base Command

microsoft-teams-auth-reset

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

microsoft-teams-token-permissions-list


Retrieves the API permissions associated with the used graph access token.

Use this command if you encounter insufficient permissions error when attempting to execute an integration command. Compare the permissions list obtained for the token with the permissions required for the desired command (can be found in the integration documentation). If there are missing API permissions, add them to your application, and then run the microsoft-teams-auth-reset command (as described here - microsoft-teams-auth-reset docs).

Base Command

microsoft-teams-token-permissions-list

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!microsoft-teams-token-permissions-list

Human Readable Output

The API permissions obtained for the used graph access token are

Permission
Group.ReadWrite.All
User.Read.All
Channel.Create

microsoft-teams-create-messaging-endpoint


Generates the messaging endpoint, based on the server URL, the server version, and the instance configurations.

The messaging endpoint should be added to the Demisto bot configuration in Microsoft Teams as part of the prerequisites of the integration’s setup. For more information see - Integration Documentation.

Base Command

microsoft-teams-create-messaging-endpoint

Input
Argument Name Description Required
engine_url If your instance configuration involves a Cortex XSOAR engine, provide the engine’s IP (or DNS name) and the port in use in the following format - https://IP:port or http://IP:port. For example - https://my-engine.name:443, http://1.1.1.1:443. Optional
Context Output

There is no context output for this command.

Command Example

!microsoft-teams-create-messaging-endpoint

Human Readable Output

The messaging endpoint is

https://ext-viso-test.crtx-qa-uat.us.paloaltonetworks.com/xsoar/instance/execute/teams-instance

The messaging endpoint should be added to the Demisto bot configuration in Microsoft Teams as part of the prerequisites of the integration’s setup.
For more information see: Integration Documentation.”

microsoft-teams-message-update


Updates a message.

Base Command

microsoft-teams-message-update

Input
Argument Name Description Required
message_id ID of the message to update. Also referred to as Activity ID. Required
team The team in which the specified message exists. Optional
channel The channel in which the specified message exists. Optional
message The new message content. Optional
team_member The message recipient. Accepts the team member’s email address or username. Optional
format_as_card Whether or not an adaptive card is being updated. Optional
Context Output
Path Type Description
MicrosoftTeams.Message.ID String ID of the message sent.
Command Example

!microsoft-teams-message-update message_id=1737151779 team=MyTeam channel=General message="New message"

Human Readable Output

Message was sent successfully.

microsoft-teams-list-messages


Retrieves a list of messages in a chat or channel.

Notes:

  • When retrieving messages from a chat, this command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

Base Command

microsoft-teams-list-messages

Input

Argument Name Description Required
conversation_id The chat ID / group chat name (topic) / oneOnOne member (Display name/mail/UPN) / channel name. Note - the consent user must be a member of the chat. Required
team_name The channel’s team. Required if specifying a channel name in the conversation_id argument. Optional
limit The number of results to retrieve. Default is 50. Optional
order_by Orders results by lastModifiedDateTime (default) or createdDateTime in descending order. Possible values are: lastModifiedDateTime, createdDateTime. Default is lastModifiedDateTime. Optional
next_link A link that specifies a starting point to use for subsequent calls. Optional
message_id Id of a message to retrieve its replies. Supported only for channels. Optional

Context Output

Path Type Description
MicrosoftTeams.MessagesList.conversationId String The conversation’s unique identifier.
MicrosoftTeams.MessagesList.messages.id String Unique ID of the message.
MicrosoftTeams.MessagesList.messages.replyToId String ID of the parent message or root message of the thread.
MicrosoftTeams.MessagesList.messages.etag String Version number of the message.
MicrosoftTeams.MessagesList.messages.messageType String The type of message.
MicrosoftTeams.MessagesList.messages.createdDateTime String Timestamp of when the message was created.
MicrosoftTeams.MessagesList.messages.lastModifiedDateTime String Timestamp when the message is created (initial setting) or modified, including when a reaction is added or removed.
MicrosoftTeams.MessagesList.messages.lastEditedDateTime String Timestamp when edits to the message were made. Triggers an “Edited” flag in the Teams UI. If no edits are made the value is null.
MicrosoftTeams.MessagesList.messages.deletedDateTime String Timestamp when the message was deleted, or null if not deleted.
MicrosoftTeams.MessagesList.messages.subject String The subject of the message, in plaintext.
MicrosoftTeams.MessagesList.messages.summary String Summary text of the message that could be used for push notifications and summary views or fall back views.
MicrosoftTeams.MessagesList.messages.chatId String If the message was sent in a chat, represents the identity of the chat.
MicrosoftTeams.MessagesList.messages.importance String The importance of the message.
MicrosoftTeams.MessagesList.messages.locale String Locale of the message set by the client.
MicrosoftTeams.MessagesList.messages.webUrl String Link to the message in Microsoft Teams.
MicrosoftTeams.MessagesList.messages.channelIdentity String If the message was sent in a channel, represents the identity of the channel.
MicrosoftTeams.MessagesList.messages.policyViolation String Defines the properties of a policy violation set by a data loss prevention (DLP) application.
MicrosoftTeams.MessagesList.messages.eventDetail String If present, represents details of an event that happened in a chat, a channel, or a team, for example, adding new members.
MicrosoftTeams.MessagesList.messages.from String Details of the sender of the message.
MicrosoftTeams.MessagesList.messages.body.content String The content of the message.
MicrosoftTeams.MessagesList.messages.body.contentType String Plaintext/HTML representation of the content of the message.
MicrosoftTeams.MessagesList.messages.attachments String References to attached objects like files, tabs, meetings, etc.
MicrosoftTeams.MessagesList.messages.attachments.content String The content of the attachments.
MicrosoftTeams.MessagesList.messages.mentions String List of entities mentioned in the message.
MicrosoftTeams.MessagesList.messages.reactions String Reactions for this message (for example, Like).
MicrosoftTeams.MessagesListNextLink String Used if an operation returns partial results. If a response contains a NextLink element, its value specifies a starting point to use for subsequent calls.
MicrosoftTeams.MessagesListMetadata.returned_count Number The actual number of messages returned in this specific execution.
MicrosoftTeams.MessagesListMetadata.filtered_count Number The total number of messages in the system that match the specified filter criteria.

microsoft-teams-send-proactive-message


Sends a proactive message to any Microsoft Teams user across the organization.

This command enables direct communication with users without requiring them to be in a specific team or channel.

Base Command

microsoft-teams-send-proactive-message

Required Permissions
  • User.Read.All - Application and Delegated
Input
Argument Name Description Required
user_id The user to send the message to. Accepts any of the following identifiers:
- Email address (e.g., “john.doe@example.com”)
- User Principal Name (e.g., “john.doe@domain.onmicrosoft.com”)
- Display name (e.g., “John Doe”)
- User GUID (e.g., “3fa9f28b-eb0e-463a-ba7b-8089fe9991e2”)

The command will automatically resolve the identifier to the user’s ID using Microsoft Graph API.
Required
message The text message to send to the user. Optional
adaptive_card The Microsoft Teams adaptive card to send (in JSON format). Optional

Note: Either message or adaptive_card must be provided, but not both.

Context Output
Path Type Description
MicrosoftTeams.Conversation.ConversationId String The ID of the conversation created or used.
MicrosoftTeams.Conversation.UserId String The resolved user ID.
MicrosoftTeams.Conversation.ActivityId String The ID of the sent message activity.
MicrosoftTeams.Conversation.UserIdentifier String The original user identifier provided.
Command Example

!microsoft-teams-send-proactive-message user_id="user@example.com" message="Your file has been quarantined due to security concerns."

Human Readable Output

Message was sent successfully.

Configuration parameters

  • credentials — Bot ID
  • bot_id — Bot ID
  • bot_password — Bot Password
  • auth_type — Authentication Type
  • redirect_uri — Application redirect URI (for Authorization Code mode)
  • auth_code_creds
  • team — Default team (required)
  • incident_notifications_channel — Notifications channel (required)
  • creds_certificate — Certificate (Required for HTTPS)
  • certificate — Certificate (Required for HTTPS)
  • key — Private Key (Required for HTTPS)
  • is_single_tenant_bot_type — Bot Type Converted to Single-Tenant
  • min_incident_severity — Minimum incident severity to send notifications to Teams by
  • auto_notifications — Disable Automatic Notifications
  • allow_external_incidents_creation — Allow external users to create incidents via direct message
  • external_form_url_header — The header of an external form hyperlink.
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • new_incident_welcome_message — Custom New Incident Welcome Message
  • longRunning — Long running instance
  • longRunningPort — Listen port
  • incidentType — Incident type

Commands (28)

  • add-user-to-channel Deprecated

    Adds a member (user) to a private/shared channel.

  • close-channel

    Deletes the specified Microsoft Teams channel.

  • create-channel Deprecated

    Creates a new channel in a Microsoft Teams team.

  • microsoft-teams-add-user-to-channel

    Adds a member (user) to a private/shared channel.

  • microsoft-teams-auth-reset

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

  • microsoft-teams-auth-test

    Tests the connectivity to MicrosoftTeams.

  • microsoft-teams-channel-user-list

    Retrieves a list of members from a channel.

  • microsoft-teams-chat-add-user

    Adds a member (user) to a group chat. Notes: - This command works with the consent user, not with the bot. Which means, the member will be added to the given chat by the consent user, not the bot. - This command will fail if the consent user is not a member of the destination chat.

  • microsoft-teams-chat-create

    Creates a new chat. Note: Only one oneOnOne chat can exist between two members. If a oneOnOne chat already exists, it will be returned.

  • microsoft-teams-chat-list

    Retrieves a list of chats that the consent user is a member of. If 'chat' is specified, retrieves this chat only.

  • microsoft-teams-chat-member-list

    Retrieves a list of members from a chat. Notes: - This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

  • microsoft-teams-chat-message-list

    Retrieves a list of messages in a chat. Notes: - This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

  • microsoft-teams-chat-update

    Updates the chat name. It can only be set for group chats. Notes: - This command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

  • microsoft-teams-create-channel

    Creates a new channel in a Microsoft Teams team.

  • microsoft-teams-create-meeting

    Creates a new meeting in Microsoft Teams.

  • microsoft-teams-create-messaging-endpoint

    Generates the messaging endpoint, based on the server URL, the server version, and the instance configurations. Note: The messaging endpoint should be added to the Demisto bot configuration in Microsoft Teams as part of the prerequisites of the integration's setup. For more information, see - https://xsoar.pan.dev/docs/reference/integrations/microsoft-teams#create-the-demisto-bot-in-microsoft-teams.

  • microsoft-teams-generate-login-url

    Generate the login url used for Authorization code flow.

  • microsoft-teams-integration-health

    Returns real-time and historical data on the integration status.

  • microsoft-teams-list-messages

    Retrieves a list of messages in a chat or channel. Notes: - When retrieving messages from a chat, this command works with the consent user, not with the bot. Which means, that the chat must include the consent user.

  • microsoft-teams-message-send-to-chat

    Sends a new chat message in the specified chat. Notes: - This command works with the consent user, not with the bot. Which means, the message is sent to the given chat by the consent user, not the bot. - This command will fail if the consent user is not a member of the destination chat. - This command may fail if the bot app has not yet appeared in the "built for your org" section in teams. - This command can only send messages from type 'message'.

  • microsoft-teams-message-update

    Updates a message.

  • microsoft-teams-ring-user

    Ring a user's Teams account. Note: This is a ring only! no media will play in case the generated call is answered. To use this make sure your Bot has the following permissions - Calls.Initiate.All and Calls.InitiateGroupCall.All

  • microsoft-teams-send-proactive-message

    Sends a proactive message to any Microsoft Teams user across the organization. This command enables direct communication with users without requiring them to be in a specific team or channel. Note: Either 'message' or 'adaptive_card' must be provided, but not both.

  • microsoft-teams-token-permissions-list

    Retrieves the API permissions associated with the used graph access token. Note: Use this command if you encounter insufficient permissions error when attempting to execute an integration command. Compare the permissions list obtained for the token with the permissions required for the desired command (can be found in the integration documentation). If there are missing API permissions, add them to your application, and then run the `microsoft-teams-auth-reset` command (as described here - https://xsoar.pan.dev/docs/reference/integrations/microsoft-teams#microsoft-teams-auth-reset).

  • microsoft-teams-user-remove-from-channel

    Removes a member (user) from a private/shared channel.

  • mirror-investigation

    Mirrors the XSOAR investigation to the specified Microsoft Teams channel. Supports only standard channels.

  • send-notification

    Sends a message to the specified teams. To mention a user in the message, add a semicolon ";" at the end of the user mention. For example: @Bruce Willis;. If sending a reply to a message, the message ID must be provided and the reply will be sent via the Graph API which means the message will appear from the account used to authorize the integration instance and not the bot. Setting the account's name and picture to match the bot will make it appear to be from the same source.

  • teams-send-notification-quick-action

    Sends a message to the specified teams.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

""" IMPORTS """
import re
import time
import urllib.parse
from collections import OrderedDict
from enum import Enum
from re import Match
from ssl import PROTOCOL_TLSv1_2, SSLContext, SSLError
from tempfile import NamedTemporaryFile
from threading import Thread
from traceback import format_exc
from typing import Any, cast

import jwt
import requests
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from flask import Flask, Response, request
from gevent.pywsgi import WSGIServer
from jwt.algorithms import RSAAlgorithm
from jwt.types import Options as JWTOptions

# Disable insecure warnings
requests.packages.urllib3.disable_warnings()  # type: ignore


class FormType(Enum):  # Used for 'send-message', and by the MicrosoftTeamsAsk script
    PREDEFINED_OPTIONS = "predefined-options"
    OPEN_ANSWER = "open-answer"


""" GLOBAL VARIABLES"""
EXTERNAL_FORM_URL_DEFAULT_HEADER = "Microsoft Teams Form"
PARAMS: dict = demisto.params()
BOT_ID: str = PARAMS.get("credentials", {}).get("identifier", "") or PARAMS.get("bot_id", "")
IS_SINGLE_TENANT_BOT_TYPE: bool = PARAMS.get("is_single_tenant_bot_type") or False
BOT_PASSWORD: str = PARAMS.get("credentials", {}).get("password", "") or PARAMS.get("bot_password", "")
APP: Flask = Flask("demisto-teams")
PLAYGROUND_INVESTIGATION_TYPE: int = 9
GRAPH_BASE_URL: str = "https://graph.microsoft.com"
CERTIFICATE = replace_spaces_in_credential(PARAMS.get("creds_certificate", {}).get("identifier", "")) or demisto.params().get(
    "certificate", ""
)
PRIVATE_KEY = replace_spaces_in_credential(PARAMS.get("creds_certificate", {}).get("password", "")) or demisto.params().get(
    "key", ""
)

INCIDENT_TYPE: str = PARAMS.get("incidentType", "")
URL_REGEX = r"(?<!\]\()https?://[^\s]*"
XSOAR_ENGINE_URL_REGEX = r"\bhttps?://(?:\w+[\w.-]*\w+|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+(?:/(?:\w+/)*\w+)?"
ENTITLEMENT_REGEX: str = r"(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}"
MENTION_REGEX = r"^@([^@;]+);| @([^@;]+);"
ENTRY_FOOTER: str = "From Microsoft Teams"
INCIDENT_NOTIFICATIONS_CHANNEL = "incidentNotificationChannel"

MESSAGE_TYPES: dict = {
    "mirror_entry": "mirrorEntry",
    "incident_opened": "incidentOpened",
    "status_changed": "incidentStatusChanged",
}

NEW_INCIDENT_WELCOME_MESSAGE: str = "Successfully created incident <incident_name>.\nView it on: <incident_link>"
MISS_CONFIGURATION_ERROR_MESSAGE = (
    "Did not receive a tenant ID from Microsoft Teams. Verify that the messaging endpoint in the "
    "Demisto bot configuration in Microsoft Teams is configured correctly.\nUse the "
    "`microsoft-teams-create-messaging-endpoint` command to get the correct messaging endpoint "
    "based on the server URL, the server version, and the instance configurations.\n"
    "For more information See - "
    "https://xsoar.pan.dev/docs/reference/integrations/microsoft-teams#troubleshooting."
)

CLIENT_CREDENTIALS_FLOW = "Client Credentials"
AUTHORIZATION_CODE_FLOW = "Authorization Code"
CREDENTIALS_TOKEN_PARAMS = "credentials_token_params"
AUTHCODE_TOKEN_PARAMS = "authcode_token_params"
AUTH_TYPE = PARAMS.get("auth_type", CLIENT_CREDENTIALS_FLOW)

AUTH_CODE: str = PARAMS.get("auth_code_creds", {}).get("password")
REDIRECT_URI: str = PARAMS.get("redirect_uri", "")
SESSION_STATE = "session_state"
REFRESH_TOKEN = "refresh_token"

CLIENT_CREDENTIALS = "client_credentials"
AUTHORIZATION_CODE = "authorization_code"

CHANNEL_SPECIAL_MARKDOWN_HEADERS: dict = {
    "id": "Membership id",
    "roles": "User roles",
    "visibleHistoryStartDateTime": "Start DateTime",
}

CHAT_SPECIAL_MARKDOWN_HEADERS: dict = {
    "id": "Chat Id",
    "topic": "Chat name",
    "webUrl": "webUrl",
    "roles": "User roles",
    "displayName": "Name",
}

USER_TYPE_TO_USER_ROLE = {"Guest": "guest", "Member": "owner"}

GROUP_CHAT_ID_SUFFIX = "@thread.v2"
ONEONONE_CHAT_ID_SUFFIX = "@unq.gbl.spaces"
MAX_ITEMS_PER_RESPONSE = 50

EXTERNAL_FORM = "external/form"
MAX_SAMPLES = 10
PROACTIVE_CACHE_MAX_SIZE = 500

TOKEN_EXPIRED_ERROR_CODES = {
    50173,
    700082,
}  # See: https://login.microsoftonline.com/error?code=
REGEX_SEARCH_ERROR_DESC = r"^[^:]*:\s(?P<desc>.*?\.)"


class GraphPermissions(str, Enum):
    """
    Graph permission names that are relevant for the various integration commands.
    """

    GROUP_READWRITE_ALL = "Group.ReadWrite.All"
    GROUP_READ_ALL = "Group.Read.All"
    GROUPMEMBER_READ_ALL = "GroupMember.Read.All"
    USER_READWRITE_ALL = "User.ReadWrite.All"
    USER_READ = "User.Read"
    USER_READ_ALL = "User.Read.All"
    CHANNEL_READBASIC_ALL = "Channel.ReadBasic.All"
    CHANNEL_DELETE_ALL = "Channel.Delete.All"
    CHANNEL_CREATE = "Channel.Create"
    CHANNELMEMBER_READ_ALL = "ChannelMember.Read.All"
    CHANNELMEMBER_READWRITE_ALL = "ChannelMember.ReadWrite.All"
    CHANNELMESSAGE_READ_ALL = "ChannelMessage.Read.All"
    CHANNELMESSAGE_SEND = "ChannelMessage.Send"
    CHAT_READ = "Chat.Read"
    CHAT_READBASIC = "Chat.ReadBasic"
    CHAT_READWRITE = "Chat.ReadWrite"
    CHAT_READWRITE_ALL = "Chat.ReadWrite.All"
    CHAT_CREATE = "Chat.Create"
    CHATMEMBER_READWRITE = "ChatMember.ReadWrite"
    CHATMESSAGE_SEND = "ChatMessage.Send"
    ONLINEMEETINGS_READWRITE = "OnlineMeetings.ReadWrite"
    ONLINEMEETINGS_READWRITE_ALL = "OnlineMeetings.ReadWrite.All"
    TEAMSAPPINSTALLATION_READWRITESELFFORCHAT = "TeamsAppInstallation.ReadWriteSelfForChat"
    APPCATALOG_READ_ALL = "AppCatalog.Read.All"
    CALLS_INITIATE_ALL = "Calls.Initiate.All"


Perms = GraphPermissions  # alias for brevity
COMMANDS_REQUIRED_PERMISSIONS: dict[str, dict[str, list[GraphPermissions]]] = {
    AUTHORIZATION_CODE_FLOW: {
        "send-notification": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL, Perms.CHANNELMESSAGE_SEND],
        "teams-send-notification-quick-action": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMESSAGE_SEND,
        ],
        "mirror-investigation": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNEL_CREATE,
            Perms.CHANNEL_DELETE_ALL,
        ],
        "close-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL, Perms.CHANNEL_DELETE_ALL],
        "microsoft-teams-ring-user": [],
        "microsoft-teams-add-user-to-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.USER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "add-user-to-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.USER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "microsoft-teams-create-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.USER_READ_ALL, Perms.CHANNEL_CREATE],
        "create-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.USER_READ_ALL, Perms.CHANNEL_CREATE],
        "microsoft-teams-create-meeting": [Perms.USER_READ_ALL, Perms.ONLINEMEETINGS_READWRITE],
        "microsoft-teams-user-remove-from-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "microsoft-teams-channel-user-list": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READ_ALL,
        ],
        "microsoft-teams-chat-create": [
            Perms.USER_READ_ALL,
            Perms.CHAT_CREATE,
            Perms.APPCATALOG_READ_ALL,
            Perms.TEAMSAPPINSTALLATION_READWRITESELFFORCHAT,
        ],
        "microsoft-teams-message-send-to-chat": [
            Perms.USER_READ_ALL,
            Perms.CHAT_READBASIC,
            Perms.CHAT_CREATE,
            Perms.CHATMESSAGE_SEND,
            Perms.APPCATALOG_READ_ALL,
            Perms.TEAMSAPPINSTALLATION_READWRITESELFFORCHAT,
        ],
        "microsoft-teams-chat-add-user": [Perms.CHAT_READBASIC, Perms.CHATMEMBER_READWRITE],
        "microsoft-teams-chat-member-list": [Perms.USER_READ_ALL, Perms.CHAT_READBASIC],
        "microsoft-teams-chat-list": [Perms.USER_READ_ALL, Perms.CHAT_READBASIC],
        "microsoft-teams-chat-message-list": [Perms.USER_READ_ALL, Perms.CHAT_READ],
        "microsoft-teams-list-messages": [
            Perms.USER_READ_ALL,
            Perms.CHAT_READ,
            Perms.CHANNELMESSAGE_READ_ALL,
        ],
        "microsoft-teams-chat-update": [Perms.USER_READ_ALL, Perms.CHAT_READWRITE],
        "microsoft-teams-message-update": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
        "microsoft-teams-integration-health": [],
        "microsoft-teams-create-messaging-endpoint": [],
        "microsoft-teams-generate-login-url": [],
        "microsoft-teams-auth-test": [],
        "microsoft-teams-auth-reset": [],
        "microsoft-teams-token-permissions-list": [],
        "microsoft-teams-send-proactive-message": [Perms.USER_READ_ALL],
    },
    CLIENT_CREDENTIALS_FLOW: {
        "send-notification": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
        "teams-send-notification-quick-action": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
        "mirror-investigation": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNEL_CREATE,
            Perms.CHANNEL_DELETE_ALL,
        ],
        "close-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL, Perms.CHANNEL_DELETE_ALL],
        "microsoft-teams-ring-user": [Perms.USER_READ_ALL, Perms.CALLS_INITIATE_ALL],
        "microsoft-teams-add-user-to-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.USER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "add-user-to-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.USER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "microsoft-teams-create-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.USER_READ_ALL, Perms.CHANNEL_CREATE],
        "create-channel": [Perms.GROUPMEMBER_READ_ALL, Perms.USER_READ_ALL, Perms.CHANNEL_CREATE],
        "microsoft-teams-create-meeting": [Perms.USER_READ_ALL, Perms.ONLINEMEETINGS_READWRITE],
        "microsoft-teams-user-remove-from-channel": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READWRITE_ALL,
        ],
        "microsoft-teams-channel-user-list": [
            Perms.GROUPMEMBER_READ_ALL,
            Perms.CHANNEL_READBASIC_ALL,
            Perms.CHANNELMEMBER_READ_ALL,
        ],
        "microsoft-teams-chat-create": [],
        "microsoft-teams-message-send-to-chat": [],
        "microsoft-teams-chat-add-user": [],
        "microsoft-teams-chat-member-list": [],
        "microsoft-teams-chat-list": [],
        "microsoft-teams-chat-message-list": [],
        "microsoft-teams-list-messages": [Perms.CHANNELMESSAGE_READ_ALL],
        "microsoft-teams-chat-update": [],
        "microsoft-teams-message-update": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
        "microsoft-teams-integration-health": [],
        "microsoft-teams-create-messaging-endpoint": [],
        "microsoft-teams-generate-login-url": [],
        "microsoft-teams-auth-test": [],
        "microsoft-teams-auth-reset": [],
        "microsoft-teams-token-permissions-list": [],
        "microsoft-teams-send-proactive-message": [Perms.USER_READ_ALL],
    },
}
HIGHER_PERMISSIONS: dict[GraphPermissions, list[GraphPermissions]] = {
    # dict with some elevated permissions and some of the permissions they can replace
    # Note: This dict is not meant to be comprehensive, rather handle possible common cases of substitute permissions.
    Perms.CHAT_READWRITE: [Perms.CHATMESSAGE_SEND, Perms.CHAT_READ, Perms.CHAT_READBASIC, Perms.CHAT_CREATE],
    Perms.CHAT_READWRITE_ALL: [
        Perms.CHAT_READWRITE,
        Perms.CHAT_READ,
        Perms.CHATMESSAGE_SEND,
        Perms.CHAT_READBASIC,
        Perms.CHAT_CREATE,
    ],
    Perms.CHAT_READ: [Perms.CHAT_READBASIC],
    Perms.USER_READ_ALL: [Perms.USER_READ],
    Perms.USER_READWRITE_ALL: [Perms.USER_READ_ALL, Perms.USER_READ],
    Perms.ONLINEMEETINGS_READWRITE_ALL: [Perms.ONLINEMEETINGS_READWRITE],
    Perms.GROUP_READ_ALL: [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
    Perms.GROUP_READWRITE_ALL: [
        Perms.GROUP_READ_ALL,
        Perms.GROUPMEMBER_READ_ALL,
        Perms.CHANNEL_CREATE,
        Perms.CHANNEL_READBASIC_ALL,
        Perms.CHANNEL_DELETE_ALL,
        Perms.CHANNELMESSAGE_SEND,
    ],
    Perms.CHANNELMEMBER_READWRITE_ALL: [Perms.CHANNELMEMBER_READ_ALL],
}

# must be synced with ones in TeamsAsk
MS_TEAMS_ASK_MESSAGE_KEYS = {"message_text", "options", "entitlement", "investigation_id", "task_id", "form_type"}


class Handler:
    @staticmethod
    def write(msg: str):
        demisto.info(msg)


class ErrorHandler:
    @staticmethod
    def write(msg: str):
        demisto.error(f"wsgi error: {msg}")


DEMISTO_LOGGER: Handler = Handler()
ERROR_LOGGER: ErrorHandler = ErrorHandler()


def handle_teams_proxy_and_ssl():
    proxies = None
    use_ssl = not PARAMS.get("insecure", False)
    if not is_demisto_version_ge("8.0.0"):
        return proxies, use_ssl
    CRTX_HTTP_PROXY = os.environ.get("CRTX_HTTP_PROXY", None)
    if CRTX_HTTP_PROXY:
        proxies = {"http": CRTX_HTTP_PROXY, "https": CRTX_HTTP_PROXY}
        use_ssl = True
    return proxies, use_ssl


PROXIES, USE_SSL = handle_teams_proxy_and_ssl()

""" HELPER FUNCTIONS """


def lru_cache_get(cache: OrderedDict, key: str) -> Any | None:
    """
    Retrieves a value from an LRU cache OrderedDict, moving the accessed key to the end
    (most-recently-used position).

    :param cache: The OrderedDict acting as the LRU cache.
    :param key: The key to look up.
    :return: The cached value, or None if the key is not present.
    """
    if key not in cache:
        return None
    cache.move_to_end(key)
    return cache[key]


def lru_cache_set(cache: OrderedDict, key: str, value: Any, max_size: int = PROACTIVE_CACHE_MAX_SIZE) -> None:
    """
    Inserts or updates a key/value pair in an LRU cache OrderedDict.
    If the cache exceeds *max_size* after insertion, the least-recently-used
    entry (the first item) is evicted.

    :param cache: The OrderedDict acting as the LRU cache.
    :param key: The key to insert or update.
    :param value: The value to store.
    :param max_size: Maximum number of entries allowed in the cache (default: PROACTIVE_CACHE_MAX_SIZE).
    """
    cache[key] = value
    cache.move_to_end(key)
    if len(cache) > max_size:
        evicted_key, _ = cache.popitem(last=False)
        demisto.debug(f"LRU cache eviction: removed oldest entry with key '{evicted_key}' (cache size limit: {max_size})")


def epoch_seconds(d: datetime = None) -> int:
    """
    Return the number of seconds for given date. If no date, return current.
    :param d: timestamp datetime object
    :return: timestamp in epoch
    """
    if not d:
        d = datetime.utcnow()
    return int((d - datetime.utcfromtimestamp(0)).total_seconds())


def error_parser(resp_err: requests.Response, api: str = "graph") -> str:
    """
    Parses Microsoft API error message from Requests response
    :param resp_err: response with error
    :param api: API to query (graph/bot)
    :return: string of error
    """
    try:
        response: dict = resp_err.json()
        demisto.debug(f"Error response from {api=}: {response=}")
        if api == "graph":
            error_codes = response.get("error_codes", [""])
            if set(error_codes).issubset(TOKEN_EXPIRED_ERROR_CODES):
                reset_auth(error_codes, response.get("error_description", ""), graph_only=True)

            error = response.get("error", {})
            err_str = (
                f"{error.get('code', '')}: {error.get('message', '')}"
                if isinstance(error, dict)
                else response.get("error_description", "")
            )
            if err_str:
                return err_str
        elif api == "bot":
            error_description: str = response.get("error_description", "")
            if error_description:
                return error_description
        # If no error message
        raise ValueError
    except ValueError:
        return resp_err.text


def reset_auth(error_codes: list = [], error_desc: str = "", graph_only: bool = False):
    """
    Reset the cached API authorization data in the integration context.
    This function clears the current authorization data: current graph/bot tokens, token validity, refresh tokens and bot type
    :param error_codes: Error codes to log when resetting after token expiration
    :param error_desc: Error description to output when resetting after token expiration
    :param graph_only: Boolean to determine if only graph tokens should be reset
    """

    integration_context: dict = get_integration_context()
    integration_context.pop("current_refresh_token", "")
    integration_context.pop("graph_access_token", "")
    integration_context.pop("graph_valid_until", "")
    integration_context[AUTHCODE_TOKEN_PARAMS] = "{}"
    integration_context[CREDENTIALS_TOKEN_PARAMS] = "{}"
    if not graph_only:
        integration_context.pop("bot_access_token", "")
        integration_context.pop("bot_valid_until", "")
        integration_context.pop("bot_type", "")
    set_integration_context(integration_context)

    if error_codes or error_desc:
        demisto.debug(f"Detected Error: {error_codes}, Successfully reset the current_refresh_token and graph_access_token.")
        re_search = re.search(REGEX_SEARCH_ERROR_DESC, error_desc)
        err_str = re_search["desc"] if re_search else ""
        raise DemistoException(
            f"{err_str} Please regenerate the 'Authorization code' "
            "parameter and then run !microsoft-teams-auth-test to re-authenticate"
        )

    demisto.debug("Successfully reset the cached API authorization data.")


def reset_auth_command():
    """
    A wrapper function for the reset_auth() which resets the cached API authorization in the integration context.
    """
    reset_auth()
    return_results(CommandResults(readable_output="Authorization was reset successfully."))


def translate_severity(severity: str) -> float:
    """
    Translates Demisto text severity to int severity
    :param severity: Demisto text severity
    :return: Demisto integer severity
    """
    severity_dictionary = {"Unknown": 0.0, "Informational": 0.5, "Low": 1.0, "Medium": 2.0, "High": 3.0, "Critical": 4.0}
    return severity_dictionary.get(severity, 0.0)


def create_incidents(demisto_user: dict, incidents: list) -> dict:
    """
    Creates incidents according to a provided JSON object
    :param demisto_user: The demisto user associated with the request (if exists)
    :param incidents: The incidents JSON
    :return: The creation result
    """
    if demisto_user:
        data = demisto.createIncidents(incidents, userID=demisto_user.get("id", ""))
    else:
        data = demisto.createIncidents(incidents)
    return data


def remove_private_info_from_body(object_to_sanitize: dict):
    """
    Some items like tenant ID are confidential and therefore should be removed from the metadata
    """
    if object_to_sanitize.get("conversation", {}).get("tenantId"):
        del object_to_sanitize["conversation"]["tenantId"]
    if object_to_sanitize.get("channelData", {}).get("tenant", {}).get("id"):
        del object_to_sanitize["channelData"]["tenant"]["id"]


def add_req_data_to_incidents(incidents: list, request_body: dict) -> list:
    """
    Adds the request_body as a rawJSON to every created incident for further information on the incident
    """
    remove_private_info_from_body(request_body)
    for incident in incidents:
        incident["rawJSON"] = json.dumps(request_body)
    return incidents


def process_incident_create_message(demisto_user: dict, message: str, request_body: dict) -> str:
    """
    Processes an incident creation message
    :param demisto_user: The Demisto user associated with the message (if exists)
    :param message: The creation message
    :param request_body: The original API request body
    :return: Creation result
    """
    json_pattern: str = r"(?<=json=).*"
    name_pattern: str = r"(?<=name=).*"
    type_pattern: str = r"(?<=type=).*"
    json_match: Match[str] | None = re.search(json_pattern, message)
    created_incident: dict | list = []
    data = ""
    if json_match:
        if re.search(name_pattern, message) or re.search(type_pattern, message):
            data = "No other properties other than json should be specified."
        else:
            incidents_json: str = json_match.group()
            incidents: dict | list = json.loads(incidents_json.replace("", '"').replace("", '"'))
            if not isinstance(incidents, list):
                incidents = [incidents]

            add_req_data_to_incidents(incidents, request_body)  # type: ignore[arg-type]
            created_incident = create_incidents(demisto_user, incidents)  # type: ignore[arg-type]
            if not created_incident:
                data = "Failed creating incidents."
    else:
        name_match: Match[str] | None = re.search(name_pattern, message)
        if not name_match:
            data = "Please specify arguments in the following manner: name=<name> type=[type] or json=<json>."
        else:
            incident_name: str = re.sub("type=.*", "", name_match.group()).strip()
            incident_type = ""

            type_match: Match[str] | None = re.search(type_pattern, message)
            if type_match:
                incident_type = re.sub("name=.*", "", type_match.group()).strip()

            incident: dict = {"name": incident_name}

            incident_type = incident_type or INCIDENT_TYPE
            if incident_type:
                incident["type"] = incident_type

            incidents = add_req_data_to_incidents([incident], request_body)
            created_incident = create_incidents(demisto_user, incidents)
            if not created_incident:
                data = "Failed creating incidents."
    if created_incident:
        update_integration_context_samples(incidents)  # type: ignore[arg-type]
        if isinstance(created_incident, list):
            created_incident = created_incident[0]
        created_incident = cast(dict[Any, Any], created_incident)
        server_links: dict = demisto.demistoUrls()
        server_link: str = server_links.get("server", "")
        server_link = server_link + "/#" if not is_demisto_version_ge("8.0.0") else server_link
        newIncidentWelcomeMessage = demisto.params().get("new_incident_welcome_message", "")
        if not newIncidentWelcomeMessage:
            newIncidentWelcomeMessage = NEW_INCIDENT_WELCOME_MESSAGE
        elif newIncidentWelcomeMessage == "no_welcome_message":
            newIncidentWelcomeMessage = ""

        if (
            newIncidentWelcomeMessage
            and ("<incident_name>" in newIncidentWelcomeMessage)
            and ("<incident_link>" in newIncidentWelcomeMessage)
        ):
            newIncidentWelcomeMessage = newIncidentWelcomeMessage.replace(
                "<incident_name>", f"{created_incident.get('name', '')}"
            ).replace("<incident_link>", f"{server_link}/WarRoom/{created_incident.get('id', '')}")
        data = newIncidentWelcomeMessage

    return data


def is_investigation_mirrored(investigation_id: str, mirrored_channels: list) -> int:
    """
    Checks if investigation is already mirrored
    :param investigation_id: Investigation ID to check if mirrored
    :param mirrored_channels: List of mirrored channels to check if investigation is mirrored in
    :return: Index in mirrored channels list if mirrored, else -1
    """
    for index, channel in enumerate(mirrored_channels):
        if channel.get("investigation_id") == investigation_id:
            return index
    return -1


def urlify_hyperlinks(message: str, url_header: str | None = EXTERNAL_FORM_URL_DEFAULT_HEADER) -> str:
    """
    Converts URLs to Markdown-format hyperlinks.
    e.g. https://www.demisto.com -> [https://www.demisto.com](https://www.demisto.com)
    :param message: Message to look for URLs in
    :return: Formatted message with hyperlinks.
    """
    url_header = url_header or EXTERNAL_FORM_URL_DEFAULT_HEADER

    def replace_url(match):
        url = match.group()
        # is the url is a survey link coming from Data Collection task
        return f"[{url_header if EXTERNAL_FORM in url else url}]({url})"

    # Replace all URLs that are not already part of markdown links
    formatted_message: str = re.sub(URL_REGEX, replace_url, message)

    return formatted_message


def get_team_member(integration_context: dict, team_member_id: str) -> dict:
    """
    Searches for a team member
    :param integration_context: Cached object to search for team member in
    :param team_member_id: Team member ID to search for
    :return: Found team member object
    """
    team_member = {}
    teams: list = json.loads(integration_context.get("teams", "[]"))

    for team in teams:
        team_members: list = team.get("team_members", [])
        for member in team_members:
            if member.get("id") == team_member_id:
                demisto.debug(f"get_team_member details: {member=}")
                team_member["username"] = member.get("name", "")
                team_member["user_email"] = member.get("email", "")
                team_member["user_principal_name"] = member.get("userPrincipalName", "")
                return team_member

    raise ValueError("Team member was not found")


def get_team_member_id(requested_team_member: str, integration_context: dict) -> str:
    """
    Gets team member ID based on name, email or principal name
    :param requested_team_member: Team member name / principal name / email to look for
    :param integration_context: Cached object to search for team member in
    :return: Team member ID
    """
    demisto.debug(f"Requested team member: {requested_team_member}")
    teams: list = json.loads(integration_context.get("teams", "[]"))
    demisto.debug(f"We've got {len(teams)} teams saved in integration context")
    for team in teams:
        team_members: list = team.get("team_members", [])
        for team_member in team_members:
            member_properties = [
                team_member.get("email", "").lower(),
                team_member.get("userPrincipalName", "").lower(),
                team_member.get("name", "").lower(),
            ]
            if requested_team_member.lower() in [value.lower() for value in member_properties]:
                return team_member.get("id")
    raise ValueError(f"Team member {requested_team_member} was not found")


def create_adaptive_card(body: list, actions: list | None = None) -> dict:
    """
    Creates Microsoft Teams adaptive card object given body and actions
    :param body: Adaptive card data
    :param actions: Adaptive card actions
    :return: Adaptive card object
    """
    adaptive_card: dict = {
        "contentType": "application/vnd.microsoft.card.adaptive",
        "content": {
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.0",
            "type": "AdaptiveCard",
            "msteams": {"width": "Full"},
            "body": body,
        },
    }
    if actions:
        adaptive_card["content"]["actions"] = actions
    return adaptive_card


def process_tasks_list(data_by_line: list) -> dict:
    """
    Processes tasks list assigned to user given from Demisto server and creates adaptive card
    :param data_by_line: List of tasks to process
    :return: Adaptive card of assigned tasks
    """
    body = []
    for line in data_by_line[2:]:
        split_data: list = [stat.strip() for stat in line.split("|")]
        body.append(
            {
                "type": "FactSet",
                "facts": [
                    {"title": "Task:", "value": split_data[0]},
                    {"title": "Incident:", "value": split_data[1]},
                    {"title": "Due:", "value": split_data[2]},
                    {"title": "Link:", "value": f"[{split_data[3]}]({split_data[3]})"},
                ],
            }
        )
    return create_adaptive_card(body)


def process_incidents_list(data_by_line: list) -> dict:
    """
    Processes incidents list assigned to user given from Demisto server and creates adaptive card
    :param data_by_line: List of incidents to process
    :return: Adaptive card of assigned incidents
    """
    body = []
    for line in data_by_line[2:]:
        split_data: list = [stat.strip() for stat in line.split("|")]
        body.append(
            {
                "type": "FactSet",
                "facts": [
                    {"title": "ID:", "value": split_data[0]},
                    {"title": "Name:", "value": split_data[1]},
                    {"title": "Status:", "value": split_data[2]},
                    {"title": "Type:", "value": split_data[3]},
                    {"title": "Owner:", "value": split_data[4]},
                    {"title": "Created:", "value": split_data[5]},
                    {"title": "Link:", "value": f"[{split_data[6]}]({split_data[6]})"},
                ],
            }
        )
    return create_adaptive_card(body)


def process_mirror_or_unknown_message(message: str) -> dict:
    """
    Processes mirror investigation command or unknown direct message and creates adaptive card
    :param message: The direct message to process
    :return: Adaptive card of mirror response / unknown message
    """
    body: list = [{"type": "TextBlock", "text": message.replace("\n", "\n\n"), "wrap": True}]
    return create_adaptive_card(body)


def is_teams_ask_message(msg: str) -> bool:
    try:
        message: dict = json.loads(msg)
        return message.keys() == MS_TEAMS_ASK_MESSAGE_KEYS
    except json.decoder.JSONDecodeError:
        return False


def process_ask_user(message: str) -> dict:
    """
    Processes ask user message and creates adaptive card
    :param message: The question object
    :return: Adaptive card of the question to send
    """
    message_object: dict = json.loads(message)
    text: str = message_object.get("message_text", "")
    entitlement: str = message_object.get("entitlement", "")
    form_type: FormType = FormType(message_object.get("form_type", FormType.PREDEFINED_OPTIONS.value))
    options: list = message_object.get("options", [])
    investigation_id: str = message_object.get("investigation_id", "")
    task_id: str = message_object.get("task_id", "")

    body: list[dict] = []
    actions: list[dict] = []

    if form_type == FormType.PREDEFINED_OPTIONS:
        body.append({"type": "TextBlock", "text": text, "wrap": True})

        for option in options:
            actions.append(
                {
                    "type": "Action.Submit",
                    "title": option,
                    "data": {
                        "response": option,
                        "entitlement": entitlement,
                        "investigation_id": investigation_id,
                        "task_id": task_id,
                    },
                }
            )

    elif form_type == FormType.OPEN_ANSWER:
        body.extend(
            [
                {
                    "type": "TextBlock",
                    "text": "Form",
                    "id": "Title",
                    "spacing": "Medium",
                    "horizontalAlignment": "Center",
                    "size": "Medium",
                    "weight": "Bolder",
                    "color": "Accent",
                    "wrap": True,
                },
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": text,
                            "wrap": True,
                            "spacing": "Medium",
                        },
                        {
                            "type": "Input.Text",
                            "placeholder": "Enter an answer",
                            "id": "response",
                            "isMultiline": True,
                        },
                    ],
                },
            ]
        )

        actions.append(
            {
                "type": "Action.Submit",
                "title": "Submit",
                "data": {"entitlement": entitlement, "investigation_id": investigation_id, "task_id": task_id},
            }
        )

    return create_adaptive_card(body, actions)


def add_data_to_actions(card_json, data_value):
    # If the current item is a list, iterate over it
    if isinstance(card_json, list):
        for item in card_json:
            add_data_to_actions(item, data_value)

    # If the current item is a dictionary
    elif isinstance(card_json, dict):
        # Check if this dictionary is an Action.Submit or Action.Execute
        if card_json.get("type") in ["Action.Submit", "Action.Execute"]:
            # Add the 'data' key with the provided value
            if "data" in card_json:
                card_json["data"].update(data_value)
            else:
                card_json["data"] = data_value

        # Only check nested elements within 'actions'
        if "actions" in card_json:
            add_data_to_actions(card_json["actions"], data_value)

        # Handle nested card inside Action.ShowCard
        if card_json.get("type") == "Action.ShowCard" and "card" in card_json:
            add_data_to_actions(card_json["card"], data_value)


def handle_raw_adaptive_card(adaptive_card_obj: dict) -> dict:
    """
    Check if the adaptive card is already wrapped with the contentType and content keys, otherwise try to fix it.

    :param adaptive_card_obj: The user supplied adaptive card
    :return: Adaptive card with contentType and content keys
    """
    if (
        "contentType" not in adaptive_card_obj
        and "content" not in adaptive_card_obj
        and adaptive_card_obj.get("type") == "AdaptiveCard"
    ):
        # This is a 'raw' adaptive card
        wrapped_adaptive_card = {
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": adaptive_card_obj,
        }
        return wrapped_adaptive_card

    # We are not completely sure this is a raw adaptive card so we will not modify it
    return adaptive_card_obj


def process_teams_ask_adaptive_card(adaptive_card_obj: dict) -> dict:
    """
    Processes adaptive cards coming from MicrosoftTeamsAsk. It will find all action elements
    of type Action.Submit or Action.Execute within adaptive_card_obj['adaptive_card'] and add entitlement,
    investigation_id and task_id to them.
    :param adaptive_card_obj: The adaptive card object.
    :return: Adaptive card with entitlement.
    """

    adaptive_card = adaptive_card_obj.get("adaptive_card", {})
    adaptive_card = handle_raw_adaptive_card(adaptive_card)
    data_obj: dict = {}
    data_obj["entitlement"] = str(adaptive_card_obj.get("entitlement", ""))
    data_obj["investigation_id"] = str(adaptive_card_obj.get("investigation_id", ""))
    data_obj["task_id"] = str(adaptive_card_obj.get("task_id", ""))

    add_data_to_actions(adaptive_card.get("content", ""), data_obj)
    return adaptive_card


def has_entitlement(adaptive_card: dict) -> bool:
    """
    Returns True if the adaptive card contains a valid TeamsAsk entitlement GUID.

    :param adaptive_card: The adaptive card object (may contain an 'entitlement' key).
    :return: True if an entitlement GUID is found, False otherwise.
    """
    return bool(re.search(ENTITLEMENT_REGEX, adaptive_card.get("entitlement", "")))


def resolve_adaptive_card(adaptive_card: dict) -> dict:
    """
    Resolves an adaptive card to its final processed form.
    If the card carries a TeamsAsk entitlement, it is processed via
    process_teams_ask_adaptive_card(); otherwise it is normalised via
    handle_raw_adaptive_card().

    :param adaptive_card: The raw adaptive card object (may contain an 'entitlement' key).
    :return: The processed adaptive card ready to be placed in a message attachment.
    """
    if has_entitlement(adaptive_card):
        return process_teams_ask_adaptive_card(adaptive_card)
    return handle_raw_adaptive_card(adaptive_card)


def get_bot_access_token() -> str:
    """
    Retrieves Bot Framework API access token, either from cache or from Microsoft
    :return: The Bot Framework API access token
    """
    integration_context: dict = get_integration_context()
    access_token: str = integration_context.get("bot_access_token", "")
    valid_until: int = integration_context.get("bot_valid_until", 0)
    bot_type: str = "single-tenant" if IS_SINGLE_TENANT_BOT_TYPE else integration_context.get("bot_type", "multi-tenant")
    if access_token and valid_until and epoch_seconds() < valid_until:
        return access_token

    data: dict = {
        "grant_type": "client_credentials",
        "client_id": BOT_ID,
        "client_secret": BOT_PASSWORD,
        "scope": "https://api.botframework.com/.default",
    }

    if bot_type == "multi-tenant":
        demisto.debug("Attempting authentication to the multi-tenant bot framework url")
        url: str = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"
        response: requests.Response = requests.post(url, data=data, verify=USE_SSL, proxies=PROXIES)
        if response.json().get("error", "") == "unauthorized_client":
            # Could not find bot-id in the common directory for multi-tenant bots, assume it is a single-tenant bot
            demisto.debug("Failed to authenticate, falling back to single-tenant bot type")
            bot_type = "single-tenant"

    if bot_type == "single-tenant":
        tenant_id = integration_context.get("tenant_id")
        if not tenant_id:
            raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)
        demisto.debug(f"Attempting authentication to the {tenant_id} tenant specific bot framework url")
        url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
        response = requests.post(url, data=data, verify=USE_SSL, proxies=PROXIES)

    if not response.ok:
        if "bot_type" in integration_context:  # Clear cached bot type on authentication error to avoid issues
            demisto.debug("Authentication failed, resetting cached bot type")
            integration_context.pop("bot_type")
            set_integration_context(integration_context)

        error = error_parser(response, "bot")
        raise ValueError(f"Failed to get bot access token [{response.status_code}] - {error}")

    try:
        response_json: dict = response.json()
        access_token = response_json.get("access_token", "")
        expires_in: int = response_json.get("expires_in", 3595)
        time_now: int = epoch_seconds()
        time_buffer = 5  # seconds by which to shorten the validity period
        if expires_in - time_buffer > 0:
            expires_in -= time_buffer
        integration_context["bot_access_token"] = access_token
        integration_context["bot_valid_until"] = time_now + expires_in
        integration_context["bot_type"] = bot_type
        set_integration_context(integration_context)
        return access_token
    except ValueError:
        raise ValueError("Failed to get bot access token")


def resolve_user_id_for_proactive_message(user_identifier: str) -> str:
    """
    Resolves a user identifier to user ID and email using Microsoft Graph API.

    :param user_identifier: User identifier - can be displayName, mail, userPrincipalName, or user ID
    :return: user_id
    """
    demisto.debug(f"Resolving user identifier: {user_identifier}")

    # Use get_user() with additional fields to get both ID and email
    users = get_user(user_identifier, select_fields="id,mail,displayName")

    if not users:
        raise ValueError(
            f"User not found: {user_identifier}. "
            f"Please provide an exact match for email, User Principal Name, or user ID (GUID)."
        )

    # Security check: If multiple users match, raise an error to prevent sending to wrong user
    if len(users) > 1:
        user_list = "\n".join(
            [f"- {u.get('displayName', 'N/A')} ({u.get('mail', 'N/A')}) - ID: {u.get('id', 'N/A')}" for u in users]
        )
        raise ValueError(
            f"Multiple users found matching '{user_identifier}':\n\n{user_list}\n\n"
            f"To avoid sending sensitive messages to the wrong user, please provide the exact user ID (GUID) "
            f"or a unique email address."
        )

    user_id = users[0].get("id", "")

    if not user_id:
        raise ValueError(f"User ID not found in response for: {user_identifier}")

    demisto.debug(f"Resolved user '{user_identifier}' to ID: {user_id}")
    return user_id


def create_proactive_conversation(user_id: str) -> str:
    """
    Creates a proactive conversation with a user using Bot Framework API.
    Checks if conversation already exists to avoid unnecessary API calls.

    :param user_id: The Microsoft Teams user ID
    :return: The conversation ID
    """
    integration_context: dict = get_integration_context()
    bot_name: str = integration_context.get("bot_name", "")
    tenant_id: str = integration_context.get("tenant_id", "")
    service_url: str = integration_context.get("service_url", "")

    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")

    if not tenant_id:
        raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)

    # Load the proactive_conversations LRU cache (OrderedDict preserves insertion order)
    raw_conversations = integration_context.get("proactive_conversations", "{}")
    raw_conversations = json.loads(raw_conversations)
    cached_conversations: OrderedDict = OrderedDict(raw_conversations)

    cached_conversation_id = lru_cache_get(cached_conversations, user_id)
    if cached_conversation_id:
        demisto.debug(f"Using cached conversation ID for user {user_id}: {cached_conversation_id}")
        return cached_conversation_id

    conversation: dict = {
        "bot": {"id": BOT_ID, "name": bot_name},
        "members": [{"id": user_id}],
        "channelData": {"tenant": {"id": tenant_id}},
    }

    url: str = f"{service_url}/v3/conversations"
    demisto.debug(f"Creating proactive conversation with user {user_id}")

    response: dict = cast(dict[Any, Any], http_request("POST", url, json_=conversation, api="bot"))
    conversation_id: str = response.get("id", "")

    demisto.debug(f"response: {response}")
    if not conversation_id:
        raise ValueError("Failed to create conversation: No conversation ID returned")

    lru_cache_set(cached_conversations, user_id, conversation_id)
    integration_context["proactive_conversations"] = json.dumps(cached_conversations)
    set_integration_context(integration_context)

    demisto.debug(f"Created and cached conversation ID: {conversation_id}")
    return conversation_id


def send_proactive_message_to_conversation(conversation_id: str, message: str = "", adaptive_card: dict = None) -> dict:
    """
    Sends a proactive message to a conversation using Bot Framework API.

    Supports @mention syntax in the message text (e.g. ``@John Smith;``).
    Mentioned users are resolved via the Microsoft Graph API since proactive
    messages may target users who are not present in the cached team members.

    :param conversation_id: The conversation ID
    :param message: The text message to send (optional if adaptive_card is provided).
                    May contain @mention syntax: ``@DisplayName;``
    :param adaptive_card: The adaptive card to send (optional if message is provided)
    :return: The API response
    """
    integration_context: dict = get_integration_context()
    service_url: str = integration_context.get("service_url", "")

    if not service_url:
        raise ValueError("Service URL not found. Please ensure the bot has been added to a team.")

    conversation_payload: dict = build_conversation_payload(
        message=message,
        adaptive_card=adaptive_card or {},
        integration_context=integration_context,
        message_id="",
        external_form_url_header=None,
        proactive=True,
    )

    url: str = f"{service_url}/v3/conversations/{conversation_id}/activities"
    demisto.debug(f"Sending proactive message to conversation {conversation_id}")

    response: dict = cast(dict[Any, Any], http_request("POST", url, json_=conversation_payload, api="bot"))
    return response


def get_refresh_token_from_auth_code_param() -> str:
    """
    The function is based on MicrosoftClient._get_refresh_token_from_auth_code_param() from 'MicrosoftApiModule'
    """
    refresh_prefix = "refresh_token:"
    if AUTH_CODE.startswith(refresh_prefix):  # for testing we allow setting the refresh token directly
        demisto.debug("Using refresh token set as auth_code")
        return AUTH_CODE[len(refresh_prefix) :]
    return ""


def get_graph_access_token(auth_type: str = "") -> str:
    """
    Retrieves Microsoft Graph API access token, either from cache or from Microsoft
    :param auth_type (str): Authentication type to use (Client credentials / Authorization code)
    :return: The Microsoft Graph API access token
    """
    auth_type = auth_type or AUTH_TYPE  # Default to integration configuration
    integration_context: dict = get_integration_context()
    if "graph_access_token" in integration_context:
        # Migrate cached tokens to new format to avoid requiring reauthentication of existing instances
        # This should happen at most once, using the global AUTH_TYPE instead of the local argument
        token_params = {
            "graph_access_token": integration_context.pop("graph_access_token", ""),
            "current_refresh_token": integration_context.pop("current_refresh_token", ""),
            "graph_valid_until": integration_context.pop("graph_valid_until", ""),
        }
        if AUTH_TYPE == CLIENT_CREDENTIALS_FLOW:
            integration_context[CREDENTIALS_TOKEN_PARAMS] = json.dumps(token_params)
        else:
            integration_context[AUTHCODE_TOKEN_PARAMS] = json.dumps(token_params)

        set_integration_context(integration_context)

    token_params_key = CREDENTIALS_TOKEN_PARAMS if auth_type == CLIENT_CREDENTIALS_FLOW else AUTHCODE_TOKEN_PARAMS
    token_params = json.loads(integration_context.get(token_params_key, "{}"))

    refresh_token = token_params.get("current_refresh_token", "")
    access_token: str = token_params.get("graph_access_token", "")
    valid_until: int = token_params.get("graph_valid_until", 0)
    if access_token and valid_until and epoch_seconds() < valid_until:
        demisto.debug("Using access token from integration context")
        return access_token
    tenant_id = integration_context.get("tenant_id")
    if not tenant_id:
        raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)
    headers = None
    url: str = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    data: dict = {
        "grant_type": CLIENT_CREDENTIALS,
        "client_id": BOT_ID,
        "scope": "https://graph.microsoft.com/.default",
        "client_secret": BOT_PASSWORD,
    }
    if auth_type == AUTHORIZATION_CODE_FLOW:
        if not AUTH_CODE:
            raise ValueError(
                "No authorization code configured. Please use the !microsoft-teams-generate-login-url command"
                " and follow the instructions to generate one."
            )
        data["redirect_uri"] = REDIRECT_URI
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        if refresh_token := refresh_token or get_refresh_token_from_auth_code_param():
            demisto.debug("Using refresh token from integration context")
            data["grant_type"] = REFRESH_TOKEN
            data["refresh_token"] = refresh_token
        else:
            if SESSION_STATE in AUTH_CODE:
                raise ValueError(
                    "Malformed auth_code parameter: Please copy the auth code from the redirected uri "
                    'without any additional info and without the "session_state" query parameter.'
                )
            data["grant_type"] = AUTHORIZATION_CODE
            data["code"] = AUTH_CODE

    response: requests.Response = requests.post(url, data=data, verify=USE_SSL, proxies=PROXIES, headers=headers)
    if not response.ok:
        error = error_parser(response)
        raise ValueError(f"Failed to get Graph access token [{response.status_code}] - {error}")
    try:
        response_json: dict = response.json()
        access_token = response_json.get("access_token", "")
        expires_in: int = response_json.get("expires_in", 3595)
        refresh_token = response_json.get("refresh_token", "")

        time_now: int = epoch_seconds()
        time_buffer = 5  # seconds by which to shorten the validity period
        if expires_in - time_buffer > 0:
            expires_in -= time_buffer

        token_params = {
            "current_refresh_token": refresh_token,
            "graph_access_token": access_token,
            "graph_valid_until": time_now + expires_in,
        }

        integration_context[token_params_key] = json.dumps(token_params)
        set_integration_context(integration_context)
        return access_token
    except ValueError:
        raise ValueError("Failed to get Graph access token")


def http_request(
    method: str, url: str = "", json_: dict = None, api: str = "graph", params: dict | None = None, auth_type: str = AUTH_TYPE
) -> dict | list:
    """A wrapper for requests lib to send our requests and handle requests and responses better
    Headers to be sent in requests

    Args:
        method (str): any restful method
        url (str): URL to query
        json_ (dict): HTTP JSON body
        api (str): API to query (graph/bot)
        params (dict): Object of key-value URL query parameters
        auth_type (str): Authentication type to use (Client credentials / Authorization code)

    Returns:
        Union[dict, list]: The response in list or dict format.
    """
    access_token = get_graph_access_token(auth_type) if api == "graph" else get_bot_access_token()  # Bot Framework API

    headers: dict = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json", "Accept": "application/json"}
    try:
        response: requests.Response = requests.request(
            method,
            url,
            headers=headers,
            json=json_,
            verify=USE_SSL,
            proxies=PROXIES,
            params=params,
        )

        if not response.ok:
            error: str = error_parser(response, api)
            if response.status_code == 403 and (detailed_error_message := insufficient_permissions_error_handler(auth_type)):
                error = f"{detailed_error_message}\n\n(Error Message): {error}"

            elif response.status_code == 401 and api == "bot":
                error = f"""\
Authorization error in call to the Microsoft Bot Framework, ensure the "Microsoft Teams" \
channel was added to the bot in the Azure portal during setup.

Refer to steps #13-14 of the "Creating the Demisto Bot using Microsoft Azure Portal" \
section in the integration documentation for more information.

If the problem is not resolved, see Troubleshooting step #5 in the integration documentation.

(Error Message): {error}"""

            raise ValueError(f"Error code [{response.status_code}] in API call to Microsoft Teams:\n{error}")

        if response.status_code in {202, 204}:
            # Delete channel or remove user from channel return 204 if successful
            # Update message returns 202 if the request has been accepted for processing
            # Create channel with a membershipType value of shared, returns 202 and a link to the teamsAsyncOperation.
            return {}
        if response.status_code == 201 and not response.content:
            # For channel creation query (with a membershipType value of standard or private), chat creation,
            # Send message in a chat, and Add member returns 201 if successful
            return {}
        try:
            return response.json()
        except ValueError:
            raise ValueError(f"Error in API call to Microsoft Teams: [{response.status_code}] - {response.text}")
    except requests.exceptions.ConnectTimeout:
        error_message = (
            "Connection Timeout Error - potential reason may be that Microsoft Teams is not accessible from your host."
        )
        raise ConnectionError(error_message)
    except requests.exceptions.SSLError:
        error_message = (
            "SSL Certificate Verification Failed - try selecting 'Trust any certificate' in the integration configuration."
        )
        raise ConnectionError(error_message)
    except requests.exceptions.ProxyError:
        error_message = (
            "Proxy Error - if 'Use system proxy settings' in the integration configuration has been "
            "selected, try deselecting it."
        )
        raise ConnectionError(error_message)


def integration_health():
    bot_framework_api_health = "Operational"
    graph_api_health = "Operational"

    try:
        get_bot_access_token()
    except ValueError as e:
        bot_framework_api_health = f"Non operational - {e!s}"

    try:
        get_graph_access_token()
    except ValueError as e:
        graph_api_health = f"Non operational - {e!s}"

    api_health_output: list = [{"Bot Framework API Health": bot_framework_api_health, "Graph API Health": graph_api_health}]

    api_health_human_readable: str = tableToMarkdown("Microsoft API Health", api_health_output)

    mirrored_channels_output = []
    integration_context: dict = get_integration_context()
    teams: list = json.loads(integration_context.get("teams", "[]"))
    for team in teams:
        mirrored_channels: list = team.get("mirrored_channels", [])
        for channel in mirrored_channels:
            mirrored_channels_output.append(
                {
                    "Team": team.get("team_name"),
                    "Channel": channel.get("channel_name"),
                    "Investigation ID": channel.get("investigation_id"),
                }
            )

    mirrored_channels_human_readable: str

    if mirrored_channels_output:
        mirrored_channels_human_readable = tableToMarkdown("Microsoft Teams Mirrored Channels", mirrored_channels_output)
    else:
        mirrored_channels_human_readable = "No mirrored channels."

    res = api_health_human_readable + mirrored_channels_human_readable
    return_results(
        CommandResults(raw_response=res, readable_output=res, entry_type=EntryType.NOTE, content_format=EntryFormat.MARKDOWN)
    )


def validate_auth_header(headers: dict) -> bool:
    """
    Validates authorization header provided in the bot activity object.
    Uses fail-close approach: returns True ONLY if ALL validations pass.
    :param headers: Bot activity headers
    :return: True if authorized, else False
    """
    parts: list = headers.get("Authorization", "").split(" ")
    if len(parts) != 2:
        error_message = "Authorization header validation failed - invalid authorization header format"
        demisto.info(error_message)
        return False
    schema: str = parts[0]
    jwt_token: str = parts[1]
    if schema != "Bearer" or not jwt_token:
        error_message = "Authorization header validation failed - failed to verify schema"
        demisto.info(error_message)
        return False

    integration_context: dict = get_integration_context()
    open_id_metadata: dict = json.loads(integration_context.get("open_id_metadata", "{}"))
    keys: list = open_id_metadata.get("keys", [])

    unverified_headers: dict = jwt.get_unverified_header(jwt_token)
    key_id: str = unverified_headers.get("kid", "")
    key_object = {}

    # Check if we got the requested key in cache
    for key in keys:
        if key.get("kid") == key_id:
            key_object = key
            break

    if not key_object:
        # Didn't find requested key in cache, getting new keys
        try:
            open_id_url: str = "https://login.botframework.com/v1/.well-known/openidconfiguration"
            response: requests.Response = requests.get(open_id_url, verify=USE_SSL, proxies=PROXIES)
            if not response.ok:
                error_message = f"Authorization header validation failed to fetch open ID config - {response.reason}"
                demisto.info(error_message)
                return False
            response_json: dict = response.json()
            jwks_uri: str = response_json.get("jwks_uri", "")
            keys_response: requests.Response = requests.get(jwks_uri, verify=USE_SSL, proxies=PROXIES)
            if not keys_response.ok:
                error_message = f"Authorization header validation failed to fetch keys - {response.reason}"
                demisto.info(error_message)
                return False
            keys_response_json: dict = keys_response.json()
            keys = keys_response_json.get("keys", [])
            open_id_metadata["keys"] = keys
        except ValueError:
            error_message = "Authorization header validation failed - failed to parse keys response"
            demisto.info(error_message)
            return False

    if not keys:
        # Didn't get new keys
        error_message = "Authorization header validation failed - failed to get keys"
        demisto.info(error_message)
        return False

    # Find requested key in new keys
    for key in keys:
        if key.get("kid") == key_id:
            key_object = key
            break

    if not key_object:
        # Didn't find requested key in new keys
        error_message = "Authorization header validation failed - failed to find relevant key"
        demisto.info(error_message)
        return False

    endorsements: list = key_object.get("endorsements", [])
    if not endorsements or "msteams" not in endorsements:
        error_message = "Authorization header validation failed - failed to verify endorsements"
        demisto.info(error_message)
        return False

    public_key = RSAAlgorithm.from_jwk(json.dumps(key_object))
    public_key: RSAPublicKey = cast(RSAPublicKey, public_key)

    options: JWTOptions = {
        "verify_aud": True,
        "verify_exp": True,
        "verify_iss": True,
        "verify_signature": True,
    }
    try:
        decoded_payload = jwt.decode(
            jwt_token, public_key, algorithms=["RS256"], options=options, audience=BOT_ID, issuer="https://api.botframework.com"
        )
    except jwt.InvalidSignatureError:
        error_message = "Authorization header validation failed - JWT signature verification failed"
        demisto.info(error_message)
        return False
    except jwt.ExpiredSignatureError:
        error_message = "Authorization header validation failed - JWT token has expired"
        demisto.info(error_message)
        return False
    except jwt.InvalidAudienceError:
        error_message = "Authorization header validation failed - Invalid audience claim"
        demisto.info(error_message)
        return False
    except jwt.InvalidIssuerError:
        error_message = "Authorization header validation failed - Invalid issuer claim"
        demisto.info(error_message)
        return False
    except jwt.PyJWTError as e:
        error_message = f"Authorization header validation failed - JWT validation error: {e}"
        demisto.info(error_message)
        return False

    # Explicit fail-close validation (defense-in-depth)
    # Even though PyJWT validates these, we explicitly check as a second layer of security
    audience_claim: str = decoded_payload.get("aud", "")
    issuer_claim: str = decoded_payload.get("iss", "")

    # Fail-close: ALL conditions must be satisfied for token to be valid
    if audience_claim == BOT_ID and issuer_claim == "https://api.botframework.com":
        # All validations passed - token is valid
        integration_context["open_id_metadata"] = json.dumps(open_id_metadata)
        set_integration_context(integration_context)
        demisto.debug(f"JWT validation successful - aud={audience_claim}, iss={issuer_claim}")
        return True

    # Explicit failure - log details for security auditing
    demisto.info(
        f"JWT validation failed (fail-close check) - "
        f"audience: expected={BOT_ID}, actual={audience_claim}, match={audience_claim == BOT_ID} | "
        f"issuer: expected='https://api.botframework.com', actual={issuer_claim}, "
        f"match={issuer_claim == 'https://api.botframework.com'} | "
    )
    return False


""" COMMANDS + REQUESTS FUNCTIONS """


def get_team_aad_id(team_name: str) -> str:
    """
    Gets Team AAD ID
    :param team_name: Team name to get AAD ID of
    :return: team AAD ID
    """
    demisto.debug(f"Team String {team_name}")
    integration_context: dict = get_integration_context()
    if integration_context.get("teams"):
        teams: list = json.loads(integration_context["teams"])
        for team in teams:
            if team_name == team.get("team_name", ""):
                demisto.debug(f"Using team_aad_id from integration context. ID - {team.get('team_aad_id', '')}")
                return team.get("team_aad_id", "")
    url: str = (
        f"{GRAPH_BASE_URL}/v1.0/groups?$filter=displayName eq '{urllib.parse.quote(team_name)}' "
        f"and resourceProvisioningOptions/Any(x:x eq 'Team')"
    )
    response: dict = cast(dict[Any, Any], http_request("GET", url))
    demisto.debug(f"Response {response}")
    teams = response.get("value", [])
    for team in teams:
        if team.get("displayName", "") == team_name:
            return team.get("id", "")
    raise ValueError("Could not find requested team.")


def get_chat_id_and_type(chat: str, create_dm_chat: bool = True) -> tuple[str, str]:
    """
    :param chat: Represents the identity of the chat - chat_name, chat_id or member in case of "oneOnOne" chat_type.
    :param create_dm_chat: Create a new one on one chat if the one requested does not exist already.
    :return: chat_id, chat_type
    """
    demisto.debug(f"Given chat: {chat}")
    url = f"{GRAPH_BASE_URL}/v1.0/chats/"

    # case1 - chat = chat_id
    if chat.endswith((GROUP_CHAT_ID_SUFFIX, ONEONONE_CHAT_ID_SUFFIX)):
        demisto.debug(f"Received chat id as chat: {chat=}")
        response: dict = cast(dict[Any, Any], http_request("GET", url + chat))  # raise 404 if the chat id was not found
        return response.get("id", ""), response.get("chatType", "")

    # case2 - chat = chat_name (topic) in case of "group" chat_type
    params = {"$filter": f"topic eq '{chat}'", "$select": "id, chatType", "$top": MAX_ITEMS_PER_RESPONSE}
    chats_response = cast(dict[Any, Any], http_request("GET", url, params=params))
    chats, _ = pages_puller(chats_response)
    if chats and chats[0]:
        demisto.debug(f"Received chat's topic as chat: {chat=}")
        return chats[0].get("id", ""), chats[0].get("chatType", "")

    # case3 - chat = member in case of "oneOnOne" chat_type.
    # Check if the given "chat" argument is representing an existing member
    user_data: list = get_user(chat)
    if not (user_data and user_data[0].get("id")):
        raise ValueError(f"Could not find chat: {chat}")

    demisto.debug(f"Received member as chat: {chat=}")
    if create_dm_chat:
        # Find the chat_id in case of 'oneOnOne' chat by calling "create_chat"
        # If a one-on-one chat already exists, this operation will return the existing chat and not create a new one
        chat_data: dict = create_chat("oneOnOne", [(user_data[0].get("id"), user_data[0].get("userType"))])
        chat_id = chat_data.get("id", "")
        chat_type = chat_data.get("chatType", "")
    else:
        # Find the chat_id without trying to create a chat, raise an error if the one-on-one chat doesn't already exist
        chat_id = get_one_on_one_chat_id(user_data[0].get("id"))
        if not chat_id:
            raise ValueError(f"Could not find chat: {chat}")
        chat_type = "oneOnOne"

    return chat_id, chat_type


def get_user(user: str, auth_type: str = AUTH_TYPE, select_fields: str = "id, userType") -> list:
    """Retrieves the AAD ID of requested user and the userType

    Args:
        user (str): Display name/mail/UPN of user to get ID of.

    Return:
        list: List containing the requsted user object
    """
    demisto.debug(f"Given user = {user}")
    url: str = f"{GRAPH_BASE_URL}/v1.0/users"
    params = {
        "$filter": f"displayName eq '{user}' or mail eq '{user}' or userPrincipalName eq '{user}'",
        "$select": select_fields,
    }
    users = cast(dict[Any, Any], http_request("GET", url, params=params, auth_type=auth_type))
    return users.get("value", [])


def add_user_to_channel(team_aad_id: str, channel_id: str, user_id: str, is_owner: bool = False):
    """
    Request for adding user to channel
    """
    url = f"{GRAPH_BASE_URL}/v1.0/teams/{team_aad_id}/channels/{channel_id}/members"
    user_role = ["owner"] if is_owner else []
    request_json: dict = create_conversation_member(user_id, user_role)
    http_request("POST", url, json_=request_json)


def add_user_to_channel_command():
    """
    Add user to channel
    This operation is allowed only for channels with a membershipType value of private or shared.
    """
    channel_name: str = demisto.args().get("channel", "")
    team_name: str = demisto.args().get("team", "")
    member = demisto.args().get("member", "")
    is_owner: bool = argToBoolean(demisto.args().get("owner", False))

    user: list = get_user(member)
    if not (user and user[0].get("id")):
        raise ValueError(f"User {member} was not found")

    team_aad_id = get_team_aad_id(team_name)
    channel_id = get_channel_id(channel_name, team_aad_id, investigation_id=None)
    if get_channel_type(channel_id, team_aad_id) == "standard":
        raise ValueError("Adding a member is allowed only for private or shared channels.")

    add_user_to_channel(team_aad_id, channel_id, user[0].get("id"), is_owner)

    demisto.results(f'The User "{member}" has been added to channel "{channel_name}" successfully.')


def create_conversation_member(user_id: str, user_role: list) -> dict:
    """
    Create a conversation member dictionary for the specified user ID.
    """
    return {
        "@odata.type": "#microsoft.graph.aadUserConversationMember",
        "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')",
        "roles": user_role,
    }


def create_channel(
    team_aad_id: str, channel_name: str, channel_description: str = "", membership_type: str = "standard", owner_id: str = ""
) -> str:
    """
    Creates a Microsoft Teams channel
    :param team_aad_id: Team AAD ID to create channel in
    :param channel_name: Name of channel to create
    :param channel_description: Description of channel to create
    :param membership_type: Channel membership type, Standard, Private or Shared. default is 'standard'
    :param owner_id: The channel owner id.
    :return: ID of created channel
    """
    url: str = f"{GRAPH_BASE_URL}/v1.0/teams/{team_aad_id}/channels"
    request_json: dict = {"displayName": channel_name, "description": channel_description, "membershipType": membership_type}

    if owner_id:
        request_json["members"] = [create_conversation_member(owner_id, ["owner"])]

    # For membershipType: standard or private - returns 201 in successful and a channel object
    # For shared, returns 202 Accepted response code and a link to the teamsAsyncOperation.
    channel_data: dict = cast(dict[Any, Any], http_request("POST", url, json_=request_json))
    channel_id: str = channel_data.get("id", "")
    return channel_id


def create_meeting(user_id: str, subject: str, start_date_time: str, end_date_time: str) -> dict:
    """
    Creates a Microsoft Teams meeting
    :param user_id: The User's ID
    :param subject: The meeting's subject
    :param start_date_time: The meeting's start time
    :param end_date_time: The meeting's end time
    :return: Dict with info about the created meeting.
    """
    url: str = f"{GRAPH_BASE_URL}/v1.0/users/{user_id}/onlineMeetings"
    request_json: dict = {"subject": subject}
    if start_date_time:
        request_json["startDateTime"] = start_date_time
    if end_date_time:
        request_json["endDateTime"] = end_date_time

    channel_data: dict = cast(dict[Any, Any], http_request("POST", url, json_=request_json))
    return channel_data


def send_message_in_chat(content: str, chat_id: str, content_type: str) -> dict:
    """
    Sends an HTTP request to send message in chat to Microsoft Teams
    :param content: The content of the chat message.
    :param message_type: The type of chat message.
    :param chat_id: The chat id
    :param content_type: The content type: html/text
    :return: dict of the chatMessage object
    """
    url = f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}/messages"
    request_json = {"body": {"content": content, "contentType": content_type}, "messageType": "message"}

    response: dict = cast(dict[Any, Any], http_request("POST", url, json_=request_json))
    return response


def chat_update_name(chat_id: str, topic: str):
    """
    Updates the chat name
    :param chat_id: The chat id
    :param topic: The new chat name
    """
    url = f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}"
    request_json = {"topic": topic}
    http_request("PATCH", url, json_=request_json)


def add_user_to_chat(chat_id: str, user_type: str, user_id: str, share_history: bool):
    """
    Adds member to given chat
    :param chat_id: The chat id
    :param user_type: The user_type: guest/member
    :param user_id: The user id
    :param share_history: whether to share history
    """

    url = f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}/members"

    request_json = {
        "@odata.type": "#microsoft.graph.aadUserConversationMember",
        "roles": [USER_TYPE_TO_USER_ROLE.get(user_type)],
        "user@odata.bind": f"https://graph.microsoft.com/v1.0/users/{user_id}",
        "visibleHistoryStartDateTime": "0001-01-01T00:00:00Z" if share_history else "",
    }

    http_request("POST", url, json_=request_json)


def pages_puller(response: dict[str, Any], limit: int = 1) -> tuple[list, str | None]:
    """
    Retrieves a limited number of pages by repeatedly making requests to the API using the nextLink URL
    until it has reached the specified limit or there are no more pages to retrieve,
    :param response: response body, contains collection of chat/message objects
    :param limit: the requested limit
    :return: tuple of the limited response_data and the last nextLink URL.
    """

    response_data = response.get("value", [])
    while (next_link := response.get("@odata.nextLink")) and len(response_data) < limit:
        demisto.debug(f"Using response {next_link=}")
        response = cast(dict[str, Any], http_request("GET", next_link))
        response_data.extend(response.get("value", []))
    demisto.debug(f"The limited response contains: {len(response_data[:limit])}")
    return response_data[:limit], next_link


def get_chats_list(odata_params: dict, chat_id: str | None = None) -> dict[str, Any]:
    """
    :param odata_params: The OData query parameters.
    :param chat_id: when chat argument was provided - Retrieve a single chat
    :return: The response body - collection of chat objects.
    """
    url = f"{GRAPH_BASE_URL}/v1.0/chats/"
    if chat_id:
        url += chat_id
    return cast(dict[str, Any], http_request("GET", url, params=odata_params))


def get_messages_list(chat_id: str, odata_params: dict) -> dict[str, Any]:
    """
    Retrieve the list of messages in a chat.
    :param chat_id: The chat_id
    :param odata_params: The OData query parameters.
    :return: The response body - collection of chatMessage objects.
    """
    url = f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}/messages"
    return cast(dict[str, Any], http_request("GET", url, params=odata_params))


def get_channel_messages_list(team_id: str, channel_id: str, odata_params: dict, message_id: str = "") -> dict[str, Any]:
    """
    Retrieve the list of messages in a channel.
    :param team_id: The team_id
    :param channel_id: The channel_id
    :param odata_params: The OData query parameters.
    :param message_id: The message_id to get its replies.
    :return: The response body - collection of chatMessage objects.
    """
    url = f"{GRAPH_BASE_URL}/v1.0/teams/{team_id}/channels/{channel_id}/messages"
    if message_id:
        url = f"{url}/{message_id}/replies"
    return cast(dict[str, Any], http_request("GET", url, params=odata_params))


def get_chat_members(chat_id: str) -> list[dict[str, Any]]:
    """
    Retrieves chat members given a chat
    :param chat_id: ID of the chat
    :return: List of chat members
    """

    url = f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}/members"
    response: dict = cast(dict[Any, Any], http_request("GET", url))
    return response.get("value", [])


def get_signed_in_user() -> dict[str, str]:
    """
    Get the properties of the signed-in user
    :return: the properties of the signed-in user
    """
    url = f"{GRAPH_BASE_URL}/v1.0/me"
    return cast(dict[str, str], http_request("GET", url))


def get_one_on_one_chat_id(user_id: str) -> str:
    """
    Retrieves the chat id for the one on one chat between the given user_id and the signed-in user.
    :param user_id: ID of the other user in the one on one chat

    :return: ID of the one on one chat
    """
    caller_id: str = get_signed_in_user().get("id", "")
    if caller_id == user_id:  # No 'one on one' between the user and himself
        return ""
    url = f"{GRAPH_BASE_URL}/v1.0/me/chats"
    params = {
        "$expand": "members",
        "$filter": (
            f"chatType eq 'oneOnOne' and members/any(m:m/microsoft.graph.aadUserConversationMember/userId eq '{user_id}')"
        ),
    }
    chats_response = cast(dict[Any, Any], http_request("GET", url, params=params))
    chats, _ = pages_puller(chats_response)
    return chats[0].get("id", "") if chats else ""


def create_chat(chat_type: str, users: list, chat_name: str = "") -> dict:
    """
    Create a new chat object.
    :param chat_type: Specifies the type of chat. Possible values are: group and oneOnOne.
    :param chat_name: The title of the chat. The chat title can be provided only if the chat is of group type.
    :param users: List of conversation members that should be added, contains the (user_id, user_type)
    :return: The chat data
    """
    demisto.debug(f"create {chat_type} chat with users = {users}")
    url = f"{GRAPH_BASE_URL}/v1.0/chats"

    # Add the caller as owner member
    caller_id: str = get_signed_in_user().get("id", "")
    members: list = [create_conversation_member(caller_id, ["owner"])]

    members += [create_conversation_member(user_id, [USER_TYPE_TO_USER_ROLE.get(user_type)]) for user_id, user_type in users]

    request_json: dict = {
        "chatType": chat_type,
        "members": members,
    }

    if chat_type == "group" and chat_name:  # The chat title can be provided only if the chat is of group type.
        request_json["topic"] = chat_name
    chat_data: dict = cast(dict[Any, Any], http_request("POST", url, json_=request_json))
    return chat_data


def create_channel_command():
    channel_name: str = demisto.args().get("channel_name", "")
    channel_description: str = demisto.args().get("description", "")
    team_name: str = demisto.args().get("team", "")
    membership_type: str = demisto.args().get("membership_type", "standard")

    owner_user = demisto.args().get("owner_user")
    if not owner_user and membership_type != "standard" and AUTH_TYPE == CLIENT_CREDENTIALS_FLOW:
        raise ValueError("When using the 'Client Credentials flow', you must specify an 'owner_user'.")

    owner_id: str = ""
    if owner_user:
        owner: list = get_user(owner_user)
        if not (owner and owner[0].get("id")):
            raise ValueError(f'The given owner_user "{owner_user}" was not found')
        owner_id = owner[0].get("id")

    team_aad_id = get_team_aad_id(team_name)
    channel_id: str = create_channel(team_aad_id, channel_name, channel_description, membership_type, owner_id)
    if channel_id or membership_type == "shared":
        demisto.results(f'The channel "{channel_name}" was created successfully')


def create_meeting_command():
    subject: str = demisto.args().get("subject", "")
    start_date_time: str = demisto.args().get("start_time", "")
    end_date_time: str = demisto.args().get("end_time", "")
    member = demisto.args().get("member", "")

    user: list = get_user(member)
    if not (user and user[0].get("id")):
        raise ValueError(f"User {member} was not found")
    meeting_data: dict = create_meeting(user[0].get("id"), subject, start_date_time, end_date_time)

    thread_id = ""
    message_id = ""
    if chat_info := meeting_data.get("chatInfo", {}):
        thread_id = chat_info.get("threadId", "")
        message_id = chat_info.get("messageId", "")

    participant_id, participant_display_name = get_participant_info(meeting_data.get("participants", {}))

    outputs = {
        "creationDateTime": meeting_data.get("creationDateTime", ""),
        "threadId": thread_id,
        "messageId": message_id,
        "id": meeting_data.get("id", ""),
        "joinWebUrl": meeting_data.get("joinWebUrl", ""),
        "participantId": participant_id,
        "participantDisplayName": participant_display_name,
    }
    result = CommandResults(
        readable_output=f'The meeting "{subject}" was created successfully',
        outputs_prefix="MicrosoftTeams.CreateMeeting",
        outputs_key_field="id",
        outputs=outputs,
    )
    return_results(result)


def channel_user_list_command():
    """
    Retrieve a list of conversationMembers from a channel.
    """
    channel_name: str = demisto.args().get("channel_name", "")
    team_name: str = demisto.args().get("team", "")
    team_aad_id = get_team_aad_id(team_name)

    channel_id = get_channel_id(channel_name, team_aad_id, investigation_id=None)
    channel_members: list = get_channel_members(team_aad_id, channel_id)
    [member.pop("@odata.type", None) for member in channel_members]
    result = CommandResults(
        readable_output=tableToMarkdown(
            f'Channel "{channel_name}" Members List:',
            channel_members,
            headers=["userId", "email", "tenantId", "id", "roles", "displayName", "visibleHistoryStartDateTime"],
            headerTransform=lambda h: CHANNEL_SPECIAL_MARKDOWN_HEADERS.get(h, pascalToSpace(h)),
        ),
        outputs_prefix="MicrosoftTeams.ChannelList",
        outputs_key_field="channelId",
        outputs={"members": channel_members, "channelName": channel_name, "channelId": channel_id},
    )
    return_results(result)


def is_bot_in_chat(chat_id: str) -> bool:
    """
    check if the bot is already in the chat.
    """

    url_suffix = f"v1.0/chats/{chat_id}/installedApps"
    res = http_request(
        "GET",
        urljoin(GRAPH_BASE_URL, url_suffix),
        params={"$expand": "teamsApp,teamsAppDefinition", "$filter": f"teamsApp/externalId eq '{BOT_ID}'"},
    )
    return bool(res.get("value"))  # type: ignore


def add_bot_to_chat(chat_id: str):
    """
    Add the Dbot to a chat.
    :param chat_id: chat id which to add the bot to.
    """
    missing_bot_err_msg = f"Bot with ID: {BOT_ID} was not found in the App Catalog in Microsoft."

    demisto.debug(f"adding bot with id {BOT_ID} to chat")

    # bot is already part of the chat
    if is_bot_in_chat(chat_id):
        demisto.debug(f"Bot is already part of the chat - chat ID: {chat_id}")
        return

    res = http_request("GET", f"{GRAPH_BASE_URL}/v1.0/appCatalogs/teamsApps", params={"$filter": f"externalId eq '{BOT_ID}'"})
    demisto.debug(f"App data is: {res}")
    if isinstance(res, dict):
        value = res.get("value", [])  # type: ignore
        if not value:
            raise DemistoException(missing_bot_err_msg)
        app_data = value[0]
        bot_internal_id = app_data.get("id")
        demisto.debug(f"Bot internal ID is: {bot_internal_id}")

        request_json = {"teamsApp@odata.bind": f"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{bot_internal_id}"}
        http_request("POST", f"{GRAPH_BASE_URL}/v1.0/chats/{chat_id}/installedApps", json_=request_json)

        demisto.debug(f"Bot {app_data.get('displayName')} with {BOT_ID} ID was added to chat successfully")

    else:
        raise DemistoException(missing_bot_err_msg)


def chat_create_command():
    """
    Create a new chat object.
    Note: Only one one-on-one chat can exist between two members.
    If a one-on-one chat already exists, this operation will return the existing chat and not create a new one.
    """
    args = demisto.args()
    chat_type: str = args.get("chat_type", "group")
    chat_name: str = args.get("chat_name", "")
    members: list = argToList(args.get("member", ""))

    # get users ids and userTypes:
    users, invalid_members = [], []
    for member in members:
        user_data: list = get_user(member)
        if not (user_data and user_data[0].get("id")):
            invalid_members.append(member)
        else:
            users.append((user_data[0].get("id"), user_data[0].get("userType")))

    if invalid_members:
        return_warning(f'The following members were not found: {", ".join(invalid_members)}')
    if chat_type == "oneOnOne" and len(users) != 1:
        raise ValueError("Creation of 'oneOnOne' chat requires 2 members. Please enter one 'member'.")

    chat_data: dict = create_chat(chat_type, users, chat_name)
    chat_data.pop("@odata.context", "")
    chat_data["chatId"] = chat_data.pop("id", "")

    add_bot_to_chat(chat_data.get("chatId", ""))

    hr_title = (
        f"The chat '{chat_name}' was created successfully"
        if chat_type == "group"
        else f'The chat with "{members[0]}" was created successfully'
    )

    result = CommandResults(
        readable_output=tableToMarkdown(
            hr_title,
            chat_data,
            headers=["chatId", "topic", "createdDateTime", "lastUpdatedDateTime", "webUrl", "tenantId"],
            url_keys=["webUrl"],
            headerTransform=lambda h: CHAT_SPECIAL_MARKDOWN_HEADERS.get(h, pascalToSpace(h)),
        ),
        outputs_prefix="MicrosoftTeams.ChatList",
        outputs_key_field="chatId",
        outputs=chat_data,
    )
    return_results(result)


def message_send_to_chat_command():
    """
    Send a new chatMessage in the specified chat.
    """
    args = demisto.args()
    content: str = args.get("content", "")
    content_type: str = args.get("content_type", "text")
    chat: str = args.get("chat", "")
    chat_id, _ = get_chat_id_and_type(chat)

    add_bot_to_chat(chat_id)

    message_data: dict = send_message_in_chat(content, chat_id, content_type)
    message_data.pop("@odata.context", "")
    hr = get_message_human_readable(message_data)
    result = CommandResults(
        readable_output=tableToMarkdown(f"Message was sent successfully in the '{chat}' chat.", hr, removeNull=True),
        outputs_prefix="MicrosoftTeams.ChatList",
        outputs_key_field="chatId",
        outputs={"messages": message_data, "chatId": chat_id},
    )
    return_results(result)


def get_message_human_readable(message_data: dict) -> dict:
    """
    Get Message human-readable.
    :param message_data: The message data
    :return: message human readable.
    """
    return {
        "Message id": message_data.get("id"),
        "Message Type": message_data.get("messageType"),
        "Etag": message_data.get("etag"),
        "Created DateTime": message_data.get("createdDateTime"),
        "lastModified DateTime": message_data.get("lastModifiedDateTime"),
        "Subject": message_data.get("subject"),
        "Chat Id": message_data.get("chatId"),
        "Importance": message_data.get("importance"),
        "Message Content": demisto.get(message_data, "body.content"),
        "Message contentType": demisto.get(message_data, "body.contentType"),
        "Initiator application": demisto.get(message_data, "eventDetail.initiator.application"),
        "Initiator device": demisto.get(message_data, "eventDetail.initiator.device"),
        "Initiator user id": demisto.get(message_data, "eventDetail.initiator.user.id"),
        "Initiator displayName": demisto.get(message_data, "eventDetail.initiator.user.displayName"),
        "Initiator userIdentityType": demisto.get(message_data, "eventDetail.initiator.user.userIdentityType"),
        "From application": demisto.get(message_data, "application"),
        "From device": demisto.get(message_data, "device"),
        "From user id": demisto.get(message_data, "from.user.id"),
        "From user": demisto.get(message_data, "from.user.displayName"),
        "From user userIdentityType": demisto.get(message_data, "from.user.userIdentityType"),
        "From user tenantId": demisto.get(message_data, "from.user.tenantId"),
    }


def chat_add_user_command():
    """
    Add a conversationMember to a chat.
    """
    args = demisto.args()
    chat: str = args.get("chat", "")
    chat_id, chat_type = get_chat_id_and_type(chat)
    if chat_type != "group":
        raise ValueError("Adding a member is allowed only on group chat.")
    members: list = argToList(args.get("member", ""))
    share_history = argToBoolean(args.get("share_history", True))

    invalid_members, hr_members = [], []
    for member in members:
        user_data: list = get_user(member)
        if not (user_data and user_data[0].get("id")):
            invalid_members.append(member)
        else:
            add_user_to_chat(chat_id, user_data[0].get("userType"), user_data[0].get("id"), share_history)
            hr_members.append(member)

    if invalid_members:
        return_warning(
            f'The following members were not found: {", ".join(invalid_members)}', exit=len(members) == len(invalid_members)
        )

    hr: str = (
        f'The Users "{", ".join(hr_members)}" have been added to chat "{chat}" successfully.'
        if len(hr_members) > 1
        else f'The User "{", ".join(hr_members)}" has been added to chat "{chat}" successfully.'
    )
    demisto.results(hr)


def chat_message_list_command():
    """
    Retrieve the list of messages in a chat.
    """
    args = demisto.args()
    chat = args.get("chat")
    chat_id, _ = get_chat_id_and_type(chat, create_dm_chat=False)
    next_link = args.get("next_link", "")

    limit = arg_to_number(args.get("limit")) or MAX_ITEMS_PER_RESPONSE
    page_size = arg_to_number(args.get("page_size")) or MAX_ITEMS_PER_RESPONSE

    top = min(MAX_ITEMS_PER_RESPONSE, limit)

    if next_link and page_size:
        limit = page_size
        messages_list_response: dict = cast(dict[str, Any], http_request("GET", next_link))
    else:
        messages_list_response = get_messages_list(
            chat_id=chat_id, odata_params={"$orderBy": args.get("order_by") + " desc", "$top": top}
        )
    messages_data, next_link = pages_puller(messages_list_response, limit)

    hr = [get_message_human_readable(message) for message in messages_data]
    result = CommandResults(
        readable_output=tableToMarkdown(f'Messages list in "{chat}" chat:', hr, url_keys=["webUrl"], removeNull=True)
        + (
            f"\nThere are more results than shown. "
            f"For more data please enter the next_link argument:\n "
            f"next_link={next_link}"
            if next_link
            else ""
        ),
        outputs_key_field="chatId",
        outputs={
            "MicrosoftTeams(true)": {"MessageListNextLink": next_link},
            "MicrosoftTeams.ChatList(val.chatId && val.chatId === obj.chatId)": {"messages": messages_data, "chatId": chat_id},
        },
    )
    return_results(result)


def resolve_chat_or_channel(
    chat_or_channel: str,
    team_name: str,
) -> tuple[str, str | None]:
    """
    Resolve whether the identifier refers to a chat or a channel.

    Returns:
        ("chat", chat_id) or ("channel", channel_id)

    Raises:
        ValueError if channel is intended but team is missing.
    """
    # First, try to resolve as channel if team_name is provided
    if team_name:
        try:
            team_id = get_team_aad_id(team_name)
            channel_id = get_channel_id(chat_or_channel, team_id, investigation_id=None)
            demisto.debug(f"Resolved {chat_or_channel} as channel {channel_id} in team {team_name}.")
            return "channel", channel_id
        except Exception:
            pass

    # If not channel, try to resolve as chat
    try:
        if AUTH_TYPE == CLIENT_CREDENTIALS_FLOW:
            # Client Credentials flow cannot resolve chats, so we skip this check
            pass
        else:
            chat_id, _ = get_chat_id_and_type(chat_or_channel, create_dm_chat=False)
            if chat_id:
                demisto.debug(f"Resolved {chat_or_channel} as chat.")
                return "chat", chat_id
    except Exception:
        pass

    error_message = "Failed to find chat or channel."
    if not team_name:
        error_message += " If you are trying to get messages from a channel, please provide the 'team_name'."
    if AUTH_TYPE == CLIENT_CREDENTIALS_FLOW:
        error_message += " If you are trying to get messages from a chat, please use the Authorization Code flow."

    raise DemistoException(error_message)


def fetch_channel_messages(
    channel_id: str,
    team_name: str,
    top: int,
    message_id: str = "",
) -> dict[str, Any]:
    team_id = get_team_aad_id(team_name)
    demisto.debug(f"Fetching messages from channel {channel_id} in team {team_name}.")
    return get_channel_messages_list(
        team_id=team_id,
        channel_id=channel_id,
        odata_params={"$top": top},
        message_id=message_id,
    )


def list_messages_command():
    """
    Retrieve the list of messages in a chat or channel.
    """
    args = demisto.args()
    chat_or_channel = args.get("conversation_id", "")
    team_name = args.get("team_name", "")
    message_id = args.get("message_id", "")
    next_link = args.get("next_link", "")

    limit = arg_to_number(args.get("limit")) or MAX_ITEMS_PER_RESPONSE
    top = min(MAX_ITEMS_PER_RESPONSE, limit)
    order_by = args.get("order_by", "lastModifiedDateTime")

    if next_link:
        messages_list_response = cast(dict[str, Any], http_request("GET", next_link))
    else:
        entity_type, resolved_id = resolve_chat_or_channel(chat_or_channel, team_name)
        if not resolved_id:
            raise ValueError(f"Could not resolve {chat_or_channel} as a valid chat or channel.")

        if entity_type == "chat":
            demisto.debug(f"Fetching messages from chat {resolved_id}.")
            messages_list_response = get_messages_list(
                chat_id=resolved_id,
                odata_params={
                    "$orderBy": f"{order_by} desc",
                    "$top": top,
                },
            )
        else:
            messages_list_response = fetch_channel_messages(
                channel_id=resolved_id,
                team_name=team_name,
                top=top,
                message_id=message_id,
            )

    messages_data, next_link = pages_puller(messages_list_response, limit)

    hr = [get_message_human_readable(message) for message in messages_data]
    result = CommandResults(
        readable_output=tableToMarkdown(f'Messages list in "{chat_or_channel}":', hr, url_keys=["webUrl"], removeNull=True)
        + (
            f"\nThere are more results than shown. "
            f"For more data please enter the next_link argument:\n "
            f"next_link={next_link}"
            if next_link
            else ""
        ),
        outputs_key_field="conversationId",
        outputs={
            "MicrosoftTeams(true)": {"MessagesListNextLink": next_link},
            "MicrosoftTeams.MessagesList(val.conversationId && val.conversationId === obj.conversationId)": {
                "messages": messages_data,
                "conversationId": chat_or_channel,
            },
            "MicrosoftTeams.MessagesListMetadata": {"returned_count": len(messages_data), "filtered_count": limit},
        },
    )
    return_results(result)


def chat_list_command():
    """
    Retrieve the list of chats that the user is part of.
    """
    args = demisto.args()
    chat = args.get("chat")
    filter_query = args.get("filter")
    next_link = args.get("next_link")
    page_size = arg_to_number(args.get("page_size")) or MAX_ITEMS_PER_RESPONSE
    limit = arg_to_number(args.get("limit")) or MAX_ITEMS_PER_RESPONSE

    if chat:
        if filter_query:
            raise ValueError("Retrieve a single chat does not support the 'filter' ODate query parameter.")
        chat_id = (
            chat
            if chat.endswith((GROUP_CHAT_ID_SUFFIX, ONEONONE_CHAT_ID_SUFFIX))
            else get_chat_id_and_type(chat, create_dm_chat=False)[0]
        )
        chats_list_response: dict = get_chats_list(odata_params={"$expand": args.get("expand")}, chat_id=chat_id)
        chats_list_response.pop("@odata.context", "")
        chats_data = [chats_list_response]
    else:
        if next_link and page_size:
            demisto.debug(f"Get chat-list using the given arguments: {next_link=} and {page_size=}")
            limit = page_size
            # the $top in the request will be as in the previous query.
            chats_list_response = cast(dict[str, Any], http_request("GET", next_link))
        else:
            demisto.debug(f"Get chat-list using the given arguments: {limit=}")
            top = min(MAX_ITEMS_PER_RESPONSE, limit)
            chats_list_response = get_chats_list(
                odata_params={"$filter": filter_query, "$expand": args.get("expand"), "$top": top}
            )

        chats_data, next_link = pages_puller(chats_list_response, limit)

    hr = [
        {**chat_data, "lastMessageReadDateTime": demisto.get(chat_data, "viewpoint.lastMessageReadDateTime")}
        for chat_data in chats_data
    ]
    for chat_data in chats_data:
        chat_data["chatId"] = chat_data.pop("id", "")
    result = CommandResults(
        readable_output=tableToMarkdown(
            ("Chat Data:" if chat else "Chats List:"),
            hr,
            url_keys=["webUrl"],
            removeNull=True,
            headers=[
                "id",
                "topic",
                "createdDateTime",
                "lastUpdatedDateTime",
                "chatType",
                "webUrl",
                "onlineMeetingInfo",
                "tenantId",
                "lastMessageReadDateTime",
            ],
            headerTransform=lambda h: CHAT_SPECIAL_MARKDOWN_HEADERS.get(h, pascalToSpace(h)),
        )
        + (
            f"\nThere are more results than shown. For more data please enter the next_link argument:\n next_link={next_link}"
            if next_link
            else ""
        ),
        outputs_key_field="chatId",
        outputs={
            "MicrosoftTeams(true)": {"ChatListNextLink": next_link},
            "MicrosoftTeams.ChatList(val.chatId && val.chatId == obj.chatId)": chats_data,
        },
    )
    return_results(result)


def chat_member_list_command():
    """
    List all conversation members in a chat.
    """

    chat: str = demisto.args().get("chat", "")
    chat_id, _ = get_chat_id_and_type(chat, create_dm_chat=False)

    chat_members: list = get_chat_members(chat_id)
    [member.pop("@odata.type", None) for member in chat_members]
    result = CommandResults(
        readable_output=tableToMarkdown(
            f'Chat "{chat}" Members List:',
            chat_members,
            headers=["userId", "roles", "displayName", "email", "tenantId"],
            headerTransform=lambda h: CHAT_SPECIAL_MARKDOWN_HEADERS.get(h, pascalToSpace(h)),
        ),
        outputs_prefix="MicrosoftTeams.ChatList",
        outputs_key_field="chatId",
        outputs={"members": chat_members, "chatId": chat_id},
    )
    return_results(result)


def chat_update_command():
    """
    Update the title of the chat.
    """
    chat: str = demisto.args().get("chat", "")
    new_name: str = demisto.args().get("chat_name", "")

    chat_id, chat_type = get_chat_id_and_type(chat)
    if chat_type != "group":
        raise ValueError("Setting chat name is allowed only on group chats.")

    chat_update_name(chat_id, new_name)
    hr = f"The name of chat '{chat}' has been successfully changed to '{new_name}'."
    return_results(hr)


def remove_user_from_channel(team_id: str, channel_id: str, membership_id: str):
    """
    Request for removing user from channel
    :param team_id: The team id
    :param channel_id: The channel id
    :param membership_id: the user membership_id
    """
    url = f"{GRAPH_BASE_URL}/v1.0/teams/{team_id}/channels/{channel_id}/members/{membership_id}"
    http_request("DELETE", url)


def get_user_membership_id(member: str, team_id: str, channel_id: str) -> str:
    """
    Searches for the given member in the channel's members and returns its membership id
    :param member: The display name of the user
    :param team_id: The team id
    :param channel_id: The channel id
    :return: the user membership_id
    """
    channel_members: list[dict[str, Any]] = get_channel_members(team_id, channel_id)
    return next(
        (user.get("id", "") for user in channel_members if user.get("displayName") == member),
        "",
    )


def user_remove_from_channel_command():
    """
    remove user from channel
    This operation is allowed only for channels with a membershipType value of private or shared.
    """
    args = demisto.args()
    channel_name: str = args.get("channel_name", "")
    team_name: str = args.get("team", "")
    member = args.get("member", "")
    team_id = get_team_aad_id(team_name)
    channel_id = get_channel_id(channel_name, team_id, investigation_id=None)
    if get_channel_type(channel_id, team_id) == "standard":
        raise ValueError("Removing a member is allowed only for private or shared channels.")

    user_membership_id = get_user_membership_id(member, team_id, channel_id)
    if not user_membership_id:
        raise ValueError(f'User "{member}" was not found in channel "{channel_name}".')

    remove_user_from_channel(team_id, channel_id, user_membership_id)
    return_results(f'The user "{member}" has been removed from channel "{channel_name}" successfully.')


def get_participant_info(participants: dict) -> tuple[str, str]:
    """
    Retrieves the participant ID and name
    :param participants: The participants in the Team meeting
    :return: The participant ID and name
    """
    participant_id = ""
    participant_display_name = ""

    if participants:
        user = participants.get("organizer", {}).get("identity", {}).get("user", {})
        if user:
            participant_id = user.get("id")
            participant_display_name = user.get("displayName")

    return participant_id, participant_display_name


def get_channel_id(channel_name: str, team_aad_id: str, investigation_id: str = None) -> str:
    """
    Retrieves Microsoft Teams channel ID
    :param channel_name: Name of channel to get ID of
    :param team_aad_id: AAD ID of team to search channel in
    :param investigation_id: Demisto investigation ID to search mirrored channel of
    :return: Requested channel ID
    """
    investigation_id = investigation_id or ""
    integration_context: dict = get_integration_context()
    teams: list = json.loads(integration_context.get("teams", "[]"))
    for team in teams:
        mirrored_channels: list = team.get("mirrored_channels", [])
        for channel in mirrored_channels:
            if channel.get("channel_name") == channel_name or channel.get("investigation_id") == investigation_id:
                return channel.get("channel_id")
    url: str = f"{GRAPH_BASE_URL}/v1.0/teams/{team_aad_id}/channels"
    response: dict = cast(dict[Any, Any], http_request("GET", url))
    demisto.debug(f"Get channels response: {json.dumps(response)}")
    channel_id: str = ""
    channels: list = response.get("value", [])
    for channel in channels:
        channel_display_name: str = channel.get("displayName", "")
        if channel_display_name == channel_name:
            channel_id = channel.get("id", "")
            break
    if not channel_id:
        raise ValueError(f"Could not find channel: {channel_name}")
    return channel_id


def get_channel_type(channel_id, team_id) -> str:
    """
    Returns the channel membershipType
    :param channel_id: The name of the channel
    :param team_id: ID of the channel's team
    :return: The channel's membershipType
    """
    url = f"{GRAPH_BASE_URL}/v1.0/teams/{team_id}/channels/{channel_id}"
    response: dict = cast(dict[Any, Any], http_request("GET", url))
    membershipType = response.get("membershipType", "standard")
    demisto.debug(f"The channel membershipType = {membershipType}")
    demisto.debug(f"Get channel response: {json.dumps(response)}")
    return membershipType


def get_team_members(service_url: str, team_id: str) -> list:
    """
    Retrieves team members given a team
    :param team_id: ID of team to get team members of
    :param service_url: Bot service URL to query
    :return: List of team members
    """
    url = f"{service_url}/v3/conversations/{team_id}/members"
    response: list = cast(list[Any], http_request("GET", url, api="bot"))
    return response


def update_integration_context_with_all_team_members(integration_context):
    """
    Retrieves all members from all teams and updates members in integration context.
    """
    service_url: str = integration_context.get("service_url", "")
    teams: list = json.loads(integration_context.get("teams", "[]"))
    for team in teams:
        team_id = team.get("team_id", "")
        team_name = team.get("team_name", "")
        demisto.debug(f"Request members for {team_id=} {team_name=}")
        url = f"{service_url}/v3/conversations/{team_id}/members"
        team_members: list = cast(list[Any], http_request("GET", url, api="bot"))
        demisto.debug(f"Updating {team_name=} with {team_members=}")
        team["team_members"] = team_members
    demisto.debug(f"Setting integration_context with {teams=}")
    integration_context["teams"] = json.dumps(teams)
    set_integration_context(integration_context)


def get_channel_members(team_id: str, channel_id: str) -> list[dict[str, Any]]:
    """
    Retrieves channel members given a channel
    :param team_id: ID of the channel's team
    :param channel_id: ID of channel to get channel members of
    :return: List of channel members
    """
    url = f"{GRAPH_BASE_URL}/v1.0/teams/{team_id}/channels/{channel_id}/members"
    response: dict = cast(dict[Any, Any], http_request("GET", url))
    return response.get("value", [])


def update_message(service_url: str, conversation_id: str, message_id: str, text: str, format_as_card: bool = True) -> dict:
    """
    Updates a message in Microsoft Teams channel
    :param service_url: Bot service URL to query
    :param conversation_id: Conversation ID of message to update
    :param message_id: ID of message to update, also referred to as Activity ID in the bot API
    :param text: Text to update in the message
    :param format_as_card: Whether to format the text as an adaptive card
    :return: dict
    """
    if format_as_card:
        body = [{"type": "TextBlock", "text": text}]
        adaptive_card: dict = create_adaptive_card(body=body)
        conversation = {"type": "message", "attachments": [adaptive_card]}
    else:
        conversation = {"type": "message", "text": text}
    url: str = f"{service_url}/v3/conversations/{conversation_id}/activities/{message_id}"  # type:ignore
    res: dict = http_request("PUT", url, json_=conversation, api="bot")  # type:ignore

    return res


def close_channel_request(team_aad_id: str, channel_id: str):
    """
    Sends an HTTP request to close a Microsoft Teams channel
    :param team_aad_id: AAD ID of team to close the channel in
    :param channel_id: ID of channel to close
    :return: None
    """
    url: str = f"{GRAPH_BASE_URL}/v1.0/teams/{team_aad_id}/channels/{channel_id}"
    http_request("DELETE", url)


def close_channel():
    """
    Deletes a mirrored Microsoft Teams channel
    """
    integration_context: dict = get_integration_context()
    channel_name: str = demisto.args().get("channel", "")
    investigation: dict = demisto.investigation()
    investigation_id: str = investigation.get("id", "")
    channel_id = ""
    team_aad_id: str
    mirrored_channels: list
    if not channel_name:
        # Closing channel as part of autoclose in mirroring process
        teams: list = json.loads(integration_context.get("teams", "[]"))
        for team in teams:
            team_aad_id = team.get("team_aad_id", "")
            mirrored_channels = team.get("mirrored_channels", [])
            for channel_index, channel in enumerate(mirrored_channels):
                if channel.get("investigation_id") == investigation_id:
                    channel_id = channel.get("channel_id", "")
                    close_channel_request(team_aad_id, channel_id)
                    mirrored_channels.pop(channel_index)
                    team["mirrored_channels"] = mirrored_channels
                    break
        if not channel_id:
            raise ValueError("Could not find Microsoft Teams channel to close.")
        integration_context["teams"] = json.dumps(teams)
        set_integration_context(integration_context)
    else:
        team_name: str = demisto.args().get("team") or demisto.params().get("team")
        team_aad_id = get_team_aad_id(team_name)
        channel_id = get_channel_id(channel_name, team_aad_id, investigation_id)
        close_channel_request(team_aad_id, channel_id)
    demisto.results("Channel was successfully closed.")


def create_personal_conversation(integration_context: dict, team_member_id: str) -> str:
    """
    Create a personal conversation with a team member
    :param integration_context: Cached object to retrieve relevant data for the conversation creation
    :param team_member_id: ID of team member to create a conversation with
    :return: ID of created conversation
    """
    bot_id: str = BOT_ID
    bot_name: str = integration_context.get("bot_name", "")
    tenant_id: str = integration_context.get("tenant_id", "")
    conversation: dict = {
        "bot": {"id": f"28:{bot_id}", "name": bot_name},
        "members": [{"id": team_member_id}],
        "channelData": {"tenant": {"id": tenant_id}},
    }
    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")
    url: str = f"{service_url}/v3/conversations"
    response: dict = cast(dict[Any, Any], http_request("POST", url, json_=conversation, api="bot"))
    return response.get("id", "")


def send_message_request(
    service_url: str, channel_id: str, conversation: dict, message_id: str = "", team_aad_id: str = ""
) -> dict:
    """
    Sends an HTTP request to send message to Microsoft Teams
    :param channel_id: ID of channel to send message in
    :param conversation: Conversation message object to send
    :param message_id: ID of message to post the reply in
    :param service_url: Bot service URL to query
    :return: dict
    """
    if not message_id:
        url: str = f"{service_url}/v3/conversations/{channel_id}/activities"
        res: dict = http_request("POST", url, json_=conversation, api="bot")  # type:ignore
    else:
        url: str = f"{GRAPH_BASE_URL}/v1.0/teams/{team_aad_id}/channels/{channel_id}/messages/{message_id}/replies"  # type:ignore
        res: dict = http_request("POST", url, json_=conversation)  # type:ignore
    return res


def process_mentioned_users_in_message(message: str) -> tuple[list, str]:
    """
    Processes the message to include all mentioned users in the right format. For example:
    Input: 'good morning @Demisto'
    Output (Formatted message): 'good morning <at>@Demisto</at>'
    :param message: The message to be processed
    :return: A list of the mentioned users, The processed message
    """
    mentioned_users: list = ["".join(user) for user in re.findall(MENTION_REGEX, message)]
    for user in mentioned_users:
        message = message.replace(f"@{user};", f"<at>@{user}</at>")
    return mentioned_users, message


def mentioned_users_to_entities(mentioned_users: list, integration_context: dict, proactive: bool = False) -> list:
    """
    Returns a list of entities built from the mentioned users.

    By default, resolves user IDs from the cached team members in the integration context
    (fast, no API call). When ``proactive=True``, resolves each user via the Microsoft
    Graph API instead — required when called from the proactive-message flow, where
    mentioned users may not be present in the team cache.

    :param mentioned_users: A list of mentioned users in the message
    :param integration_context: Cached object to retrieve relevant data from (used when proactive=False)
    :param proactive: When True, resolve user IDs via Graph API instead of the team cache.
                      Set this when calling from a proactive message context.
    :return: A list of entities
    """
    entities = []
    for user in mentioned_users:
        if proactive:
            user_id = resolve_user_id_for_proactive_message(user)
        else:
            user_id = get_team_member_id(user, integration_context)
        entities.append(
            {
                "type": "mention",
                "mentioned": {"id": user_id, "name": user},
                "text": f"<at>@{user}</at>",
            }
        )
    return entities


def build_conversation_payload(
    message: str,
    adaptive_card: dict,
    integration_context: dict,
    message_id: str = "",
    external_form_url_header: str | None = None,
    proactive: bool = False,
) -> dict:
    """
    Builds the Bot Framework conversation payload dict from either a text message
    or an adaptive card.

    Handles three cases:
    1. TeamsAsk text message  – message is a JSON blob matching MS_TEAMS_ASK_MESSAGE_KEYS.
    2. Regular text message   – plain text, optionally with @mentions and URL hyperlinking.
       When message_id is set the payload targets a channel-thread reply (Graph API format).
    3. Adaptive card only     – message is empty; adaptive_card is resolved and attached.

    :param message: The text message to send (may be empty if adaptive_card is provided).
    :param adaptive_card: The adaptive card dict (may be empty if message is provided).
    :param integration_context: Cached integration context (used for @mention resolution
                                when proactive=False).
    :param message_id: When set, formats the payload as a channel-thread reply.
                       Pass an empty string for a new top-level message.
    :param external_form_url_header: Custom header for Data Collection survey URLs.
                                     Defaults to EXTERNAL_FORM_URL_DEFAULT_HEADER when None.
    :param proactive: When True, resolves @mentions via Graph API instead of the team cache.
                      Set this when building a payload for a proactive message.
    :return: The conversation payload dict ready to be sent to the Bot Framework API.
    """
    if message:
        entitlement_match_msg: Match[str] | None = re.search(ENTITLEMENT_REGEX, message)
        if entitlement_match_msg and is_teams_ask_message(message):
            # TeamsAsk flow: build an Adaptive Card from the JSON message
            card = process_ask_user(message)
            demisto.debug(f"The following Adaptive Card will be used:\n{json.dumps(card)}")
            return {"type": "message", "attachments": [card]}

        # Regular text message
        formatted_message: str = urlify_hyperlinks(message, external_form_url_header)
        mentioned_users, formatted_message_with_mentions = process_mentioned_users_in_message(formatted_message)
        entities = mentioned_users_to_entities(mentioned_users, integration_context, proactive=proactive)
        demisto.info(f"msg: {formatted_message_with_mentions}, ent: {entities}")

        if not message_id:
            return {"type": "message", "text": formatted_message_with_mentions, "entities": entities}
        else:
            # Reply to an existing channel message (Graph API format)
            payload: dict = {"body": {"contentType": "html", "content": formatted_message_with_mentions}}
            if entities:
                payload["body"]["mentions"] = entities
            return payload

    # Adaptive card only
    # This use case is relevant for TeamsAsk script when the entitlement is one of the keys under the adaptive_card
    return {"type": "message", "attachments": [resolve_adaptive_card(adaptive_card)]}


def send_message():
    message_type: str = demisto.args().get("messageType", "")
    original_message: str = demisto.args().get("originalMessage", "")
    message: str = demisto.args().get("message", "")
    message_id: str = demisto.args().get("message_id", "")
    team_name: str = demisto.args().get("team", "") or demisto.params().get("team", "")
    external_form_url_header: str | None = demisto.args().get("external_form_url_header") or demisto.params().get(
        "external_form_url_header"
    )
    try:
        adaptive_card: dict = json.loads(demisto.args().get("adaptive_card", "{}"))
    except ValueError as e:
        raise ValueError(f"Given adaptive card is not in valid JSON format. Error: {str(e)}")

    demisto.debug(f"In send message with message type: {message_type}, and channel name:{demisto.args().get('channel')}")

    if message_type == MESSAGE_TYPES["mirror_entry"] and ENTRY_FOOTER in original_message:
        demisto.debug(f"the message '{message}' was already mirrored, skipping it")
        # Got a message which was already mirrored - skipping it
        return
    channel_name: str = demisto.args().get("channel", "")

    if (
        not channel_name and message_type in {MESSAGE_TYPES["status_changed"], MESSAGE_TYPES["incident_opened"]}
    ) or channel_name == INCIDENT_NOTIFICATIONS_CHANNEL:
        demisto.debug("Got a notification from server.")
        # Got a notification from server
        channel_name = demisto.params().get("incident_notifications_channel", "General")
        severity: float = float(demisto.args().get("severity"))

        # Adding disable and not enable because of adding new boolean parameter always defaults to false value in server
        if (disable_auto_notifications := demisto.params().get("auto_notifications")) is not None:
            disable_auto_notifications = argToBoolean(disable_auto_notifications)
        else:
            disable_auto_notifications = False

        if not disable_auto_notifications:
            severity_threshold: float = translate_severity(demisto.params().get("min_incident_severity", "Low"))
            if severity < severity_threshold:
                return
        else:
            return

    team_member: str = demisto.args().get("team_member", "") or demisto.args().get("to", "")
    if re.match(r"\b[^@]+@[^@]+\.[^@]+\b", team_member):  # team member is an email
        team_member = team_member.lower()

    if not (team_member or channel_name):
        raise ValueError("No channel or team member to send message were provided.")

    if team_member and channel_name:
        raise ValueError("Provide either channel or team member to send message to, not both.")

    if not (message or adaptive_card):
        raise ValueError("No message or adaptive card to send were provided.")

    if message and adaptive_card:
        raise ValueError("Provide either message or adaptive to send, not both.")

    integration_context: dict = get_integration_context()
    channel_id = ""
    personal_conversation_id = ""
    if team_name:
        team_aad_id: str = get_team_aad_id(team_name)
    else:
        team_aad_id = ""
    if channel_name:
        channel_id = get_channel_id_for_send_notification(team_aad_id, channel_name, message_type)
    elif team_member:
        try:
            team_member_id: str = get_team_member_id(team_member, integration_context)
        except Exception:
            demisto.debug(f"Did not find '{team_member=}' will update integration context with all team members.")
            update_integration_context_with_all_team_members(integration_context)
            team_member_id = get_team_member_id(team_member, integration_context)
        personal_conversation_id = create_personal_conversation(integration_context, team_member_id)

    recipient: str = channel_id or personal_conversation_id

    conversation: dict = build_conversation_payload(
        message=message,
        adaptive_card=adaptive_card,
        integration_context=integration_context,
        message_id=message_id,
        external_form_url_header=external_form_url_header,
        proactive=False,
    )

    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")
    res: dict = send_message_request(service_url, recipient, conversation, message_id, team_aad_id)
    results = CommandResults(
        outputs={"ID": res.get("id")},
        outputs_prefix="MicrosoftTeams.Message",
        readable_output="Message was sent successfully.",
        raw_response=res,
    )
    return_results(results)


def message_update_command():
    message: str = demisto.args().get("message", "")
    message_id: str = demisto.args().get("message_id", "")
    team_name: str = demisto.args().get("team", "") or demisto.params().get("team", "")
    channel_name: str = demisto.args().get("channel", "")
    format_as_card: bool = argToBoolean(demisto.args().get("format_as_card", "true"))

    team_member: str = demisto.args().get("team_member", "") or demisto.args().get("to", "")
    if re.match(r"\b[^@]+@[^@]+\.[^@]+\b", team_member):  # team member is an email
        team_member = team_member.lower()

    if not (team_member or channel_name):
        raise ValueError("No channel or team member to send message were provided.")

    if team_member and channel_name:
        raise ValueError("Provide either channel or team member to send message to, not both.")

    integration_context: dict = get_integration_context()
    channel_id = ""
    personal_conversation_id = ""
    if team_name:
        team_aad_id: str = get_team_aad_id(team_name)
    else:
        team_aad_id = ""
    if channel_name:
        channel_id = get_channel_id_for_send_notification(team_aad_id, channel_name, "")
    elif team_member:
        team_member_id: str = get_team_member_id(team_member, integration_context)
        personal_conversation_id = create_personal_conversation(integration_context, team_member_id)

    recipient: str = channel_id or personal_conversation_id

    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")

    res: dict = update_message(service_url, recipient, message_id, message, format_as_card=format_as_card)

    results = CommandResults(
        outputs={"ID": res.get("id")},
        outputs_prefix="MicrosoftTeams.Message",
        readable_output="Message was sent successfully.",
        raw_response=res,
    )
    return_results(results)


def send_proactive_message_command():
    """
    Sends a proactive message to any Microsoft Teams user.
    This command enables sending messages to users across the entire organization without requiring
    the user to be in a specific team or channel.

    The command:
    1. Resolves the user identifier (displayName, mail, userPrincipalName, or user ID) to the actual user ID
    2. Creates or retrieves a proactive conversation with the user
    3. Sends the message or adaptive card to the conversation
    """
    args = demisto.args()
    user_id_arg: str = args.get("user_id", "")
    message: str = args.get("message", "")
    adaptive_card_arg = args.get("adaptive_card", "")

    if not message and not adaptive_card_arg:
        raise ValueError("Either message or adaptive_card must be provided.")

    if message and adaptive_card_arg:
        raise ValueError("Provide either message or adaptive_card, not both.")

    user_id = resolve_user_id_for_proactive_message(user_id_arg)

    demisto.debug(f"Creating/retrieving proactive conversation for user: {user_id}")
    conversation_id = create_proactive_conversation(user_id)

    adaptive_card: dict = {}
    if adaptive_card_arg:
        try:
            adaptive_card = json.loads(adaptive_card_arg)
        except ValueError as e:
            raise ValueError(f"Failed to parse Adaptive Card: The provided input is not a valid JSON string. Error: {str(e)}")

    demisto.debug(f"Sending proactive message to conversation: {conversation_id}")
    response = send_proactive_message_to_conversation(
        conversation_id=conversation_id, message=message, adaptive_card=adaptive_card
    )
    demisto.debug(f"Proactive message sent successfully. ActivityId: {response.get('id', '')}")
    # Prepare outputs
    outputs = {
        "ConversationId": conversation_id,
        "UserId": user_id,
        "ActivityId": response.get("id", ""),
        "UserIdentifier": user_id_arg,
    }

    result = CommandResults(
        outputs_prefix="MicrosoftTeams.Conversation",
        outputs_key_field="ConversationId",
        outputs=outputs,
        readable_output="Message was sent successfully.",
        raw_response=response,
    )

    return_results(result)


def get_channel_id_for_send_notification(team_aad_id: str, channel_name: str, message_type: str):
    """
    Returns the channel ID to send the message to
    :param channel_name: The name of the channel.
    :param message_type: The type of message to be sent.
    :return: the channel ID
    """
    investigation_id = ""
    if message_type == MESSAGE_TYPES["mirror_entry"]:
        # Got an entry from the War Room to mirror to Teams
        # Getting investigation ID in case channel name is custom and not the default
        investigation: dict = demisto.investigation()
        investigation_id = investigation.get("id", "")
    channel_id = get_channel_id(channel_name, team_aad_id, investigation_id)
    if get_channel_type(channel_id, team_aad_id) != "standard":
        raise ValueError("Posting a message or adaptive card to a private/shared channel is currently not supported.")
    return channel_id


def mirror_investigation():
    """
    Updates the integration context with a new or existing mirror.
    """
    investigation: dict = demisto.investigation()

    if investigation.get("type") == PLAYGROUND_INVESTIGATION_TYPE:
        raise ValueError("Can not perform this action in playground.")

    integration_context: dict = get_integration_context()

    mirror_type: str = demisto.args().get("mirror_type", "all")
    auto_close: str = demisto.args().get("autoclose", "true")
    mirror_direction: str = demisto.args().get("direction", "both").lower()
    team_name: str = demisto.args().get("team", "")
    if not team_name:
        team_name = demisto.params().get("team", "")
    team_aad_id: str = get_team_aad_id(team_name)
    mirrored_channels = []
    teams: list = json.loads(integration_context.get("teams", "[]"))
    team = {}
    for team in teams:
        if team.get("team_aad_id", "") == team_aad_id:
            if team.get("mirrored_channels"):
                mirrored_channels = team["mirrored_channels"]
            break
    if mirror_direction != "both":
        mirror_type = f"{mirror_type}:{mirror_direction}"

    investigation_id: str = investigation.get("id", "")
    investigation_mirrored_index: int = is_investigation_mirrored(investigation_id, mirrored_channels)

    if investigation_mirrored_index > -1:
        # Updating channel mirror configuration
        mirrored_channels[investigation_mirrored_index]["mirror_type"] = mirror_type
        mirrored_channels[investigation_mirrored_index]["mirror_direction"] = mirror_direction
        mirrored_channels[investigation_mirrored_index]["auto_close"] = auto_close
        mirrored_channels[investigation_mirrored_index]["mirrored"] = False
        demisto.results("Investigation mirror was updated successfully.")
    else:
        channel_name: str = demisto.args().get("channel_name", "") or f"incident-{investigation_id}"
        channel_description: str = f"Channel to mirror incident {investigation_id}"
        try:
            channel_id: str = create_channel(team_aad_id, channel_name, channel_description)

        except ValueError as e:
            if "Channel name already existed" in str(e):
                channel_id = get_channel_id(channel_name, team_aad_id)
            else:
                raise e

        service_url: str = integration_context.get("service_url", "")
        server_links: dict = demisto.demistoUrls()
        server_link: str = server_links.get("server", "")
        server_link = server_link + "/#" if not is_demisto_version_ge("8.0.0") else server_link
        warroom_link = f"{server_link}/WarRoom/{investigation_id}"
        conversation: dict = {
            "type": "message",
            "text": f"This channel was created to mirror [incident {investigation_id}]({warroom_link}) "
            f"between Teams and Demisto. In order for your Teams messages to be mirrored in Demisto, "
            f"you need to mention the Demisto Bot in the message.",
        }
        send_message_request(service_url, channel_id, conversation)
        mirrored_channels.append(
            {
                "channel_id": channel_id,
                "investigation_id": investigation_id,
                "mirror_type": mirror_type,
                "mirror_direction": mirror_direction,
                "auto_close": auto_close,
                "mirrored": False,
                "channel_name": channel_name,
            }
        )
        demisto.results(f"Investigation mirrored successfully in channel {channel_name}.")
    team["mirrored_channels"] = mirrored_channels
    integration_context["teams"] = json.dumps(teams)
    set_integration_context(integration_context)


def channel_mirror_loop():
    """
    Runs in a long running container - checking for newly mirrored investigations.
    """
    while True:
        found_channel_to_mirror: bool = False
        integration_context = {}
        try:
            integration_context = get_integration_context()
            teams: list = json.loads(integration_context.get("teams", "[]"))
            for team in teams:
                mirrored_channels = team.get("mirrored_channels", [])
                channel: dict
                for channel in mirrored_channels:
                    investigation_id = channel.get("investigation_id", "")
                    if not channel["mirrored"]:
                        demisto.info(f"Mirroring incident: {investigation_id} in Microsoft Teams")
                        channel_to_update: dict = channel
                        if channel_to_update["mirror_direction"] and channel_to_update["mirror_type"]:
                            demisto.mirrorInvestigation(
                                channel_to_update["investigation_id"],
                                channel_to_update["mirror_type"],
                                bool(argToBoolean(channel_to_update["auto_close"])),
                            )
                            channel_to_update["mirrored"] = True
                            demisto.info(f"Mirrored incident: {investigation_id} to Microsoft Teams successfully")
                        else:
                            demisto.info(f"Could not mirror {investigation_id}")
                        team["mirrored_channels"] = mirrored_channels
                        integration_context["teams"] = json.dumps(teams)
                        set_integration_context(integration_context)
                        found_channel_to_mirror = True
                        break
                if found_channel_to_mirror:
                    break
        except json.decoder.JSONDecodeError as json_decode_error:
            demisto.error(
                f"An error occurred in channel mirror loop while trying to deserialize teams from cache: "
                f"{json_decode_error!s}"
            )
            demisto.updateModuleHealth(f"An error occurred: {json_decode_error!s}")
        except Exception as e:
            demisto.error(f"An error occurred in channel mirror loop: {e!s}")
            demisto.updateModuleHealth(f"An error occurred: {e!s}")
        finally:
            time.sleep(5)


def filter_invalid_teams(teams: list[dict], team_name: str, current_team_aad_id: str = "") -> list[dict]:
    """
    Requests an updated list of teams with the given team name
    and filters any missing teams of the same name from the given teams list.

    :param teams: List of teams currently cached in the integration context.
    :param team_name: Team name to test for.
    :param current_team_aad_id: ID of the team that is currently being handled.
    The Graph API can take longer to update than it takes for event messages to arrive, thus not reflecting the current change.

    :returns: Updated list of teams with bad team entries removed.
    """
    demisto.debug(f"Checking for invalid teams in cache for name '{team_name}'")
    try:
        url: str = (
            f"{GRAPH_BASE_URL}/v1.0/groups?$filter=displayName eq '{urllib.parse.quote(team_name)}' "
            f"and resourceProvisioningOptions/Any(x:x eq 'Team')"
        )
        response: dict = cast(dict[Any, Any], http_request("GET", url))
        response_teams = response.get("value", [])
        demisto.debug(f"Found {len(response_teams)} valid teams with the name {team_name}")
        valid_team_aad_ids = [team.get("id", "") for team in response_teams]
        if current_team_aad_id and current_team_aad_id not in valid_team_aad_ids:
            demisto.debug("Current team aad_id was not listed, adding it manually")
            valid_team_aad_ids.append(current_team_aad_id)
        demisto.debug(f"aad_ids of discovered valid teams: {valid_team_aad_ids}")

        invalid_teams = [
            team for team in teams if (team.get("team_name") == team_name and team.get("team_aad_id") not in valid_team_aad_ids)
        ]
        if invalid_teams:
            teams = [team for team in teams if team not in invalid_teams]
            filtered_ids = [team.get("team_aad_id", "") for team in invalid_teams]
            demisto.debug(f"Invalid team aad_ids filtered: {filtered_ids}")

        return teams

    except Exception as e:
        demisto.debug(f"Got error while trying to update the team cache: {e}")
        return teams


def member_added_handler(integration_context: dict, request_body: dict, channel_data: dict):
    """
    Handles member added activity
    :param integration_context: Cached object to retrieve relevant data from
    :param request_body: Activity payload
    :param channel_data: Microsoft Teams tenant, team and channel details
    :return: None
    """
    bot_id = BOT_ID

    team: dict = channel_data.get("team", {})
    team_id: str = team.get("id", "")
    team_aad_id: str = team.get("aadGroupId", "")
    team_name: str = team.get("name", "")

    tenant: dict = channel_data.get("tenant", {})
    tenant_id: str = tenant.get("id", "")

    recipient: dict = request_body.get("recipient", {})
    recipient_name: str = recipient.get("name", "")

    members_added: list = request_body.get("membersAdded", [])

    teams: list = json.loads(integration_context.get("teams", "[]"))

    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")

    integration_context["bot_name"] = recipient_name

    if tenant_id != integration_context.get("tenant_id"):
        # Update the tenant id in context immediately to avoid errors
        demisto.debug(f"Saving tenant ID to context: {tenant_id=}")
        integration_context["tenant_id"] = tenant_id
        set_integration_context(integration_context)

    for member in members_added:
        member_id = member.get("id", "")
        if bot_id in member_id:
            demisto.info(f"The bot was added to team {team_name}")
        else:
            demisto.info(f"A user was added to team {team_name}")

    team_members: list = get_team_members(service_url, team_id)

    found_team: bool = False
    found_duplicate_name: bool = False
    for team in teams:
        if team.get("team_aad_id", "") == team_aad_id:
            team["team_members"] = team_members
            found_team = True

        elif team.get("team_name", "") == team_name:  # Found cached team with same name but different id
            found_duplicate_name = True

    if found_duplicate_name:
        teams = filter_invalid_teams(teams, team_name)

    if not found_team:
        # Didn't found an existing team, adding new team object
        teams.append({"team_aad_id": team_aad_id, "team_id": team_id, "team_name": team_name, "team_members": team_members})
    integration_context["teams"] = json.dumps(teams)
    set_integration_context(integration_context)


def team_renamed_handler(integration_context: dict, channel_data: dict):
    """
    Updates the cached teams data with the new team name.
    Adds a new team entry to the cache if the team is not already cached.

    :param integration_context: Cache object to retrieve relevant data from.
    :param channel_data: Channel data dict containing relevant data of the event.
    :return: None
    """
    renamed_team = channel_data.get("team", {})
    renamed_team_aad_id = renamed_team.get("aadGroupId")
    team_new_name = renamed_team.get("name")
    renamed_team_id = renamed_team.get("id", "")
    demisto.debug(f"Renamed team aad_id is {renamed_team_aad_id}")

    context_teams: list = json.loads(integration_context.get("teams", "[]"))
    service_url: str = integration_context.get("service_url", "")

    found_team: bool = False
    found_duplicate_name: bool = False
    for team in context_teams:
        if team.get("team_aad_id", "") == renamed_team_aad_id:
            demisto.info(f"Team {team['team_name']} has been renamed to {team_new_name}")
            team["team_name"] = team_new_name
            found_team = True

        elif team.get("team_name", "") == team_new_name:  # Found cached team with same name but different id
            found_duplicate_name = True

    if found_duplicate_name:
        context_teams = filter_invalid_teams(context_teams, team_new_name, renamed_team_aad_id)

    if not found_team and service_url:
        demisto.debug("Did not find the team in context, creating a new cache entry")
        team_members: list = get_team_members(service_url, renamed_team_id)
        context_teams.append(
            {
                "team_aad_id": renamed_team_aad_id,
                "team_id": renamed_team_id,
                "team_name": team_new_name,
                "team_members": team_members,
            }
        )

    integration_context["teams"] = json.dumps(context_teams)
    set_integration_context(integration_context)


def team_deleted_handler(integration_context: dict, channel_data: dict):
    """
    Removes the team listed in the event from the cached teams data.

    :param integration_context: Cache object to retrieve relevant data from.
    :param channel_data: Channel data dict containing relevant data of the event.
    :return: None
    """
    team_aad_id = channel_data.get("team", {}).get("aadGroupId")
    demisto.debug(f"Deleted team aad_id is {team_aad_id}")

    teams: list = json.loads(integration_context.get("teams", "[]"))
    teams = [team for team in teams if team.get("team_aad_id", "") != team_aad_id]

    integration_context["teams"] = json.dumps(teams)
    set_integration_context(integration_context)


def handle_external_user(user_identifier: str, allow_create_incident: bool, create_incident: bool) -> str:
    """
    Handles message from non xsoar user
    :param user_identifier: the user name or email
    :param allow_create_incident: if external user is allowed to create incidents or not
    :param create_incident: if the message (command) sent by the user is "new incident"
    :return: data: the response from the bot the user
    """
    # external user is not allowed to run any command
    if not allow_create_incident:
        data = (
            f"I'm sorry but I was unable to find you as a Cortex XSOAR user "
            f"for {user_identifier}. You're not allowed to run any command"
        )

    # allowed creating new incident, but the command sent is not new incident
    elif not create_incident:
        data = "As a non Cortex XSOAR user, you're only allowed to run command:\nnew incident [details]"
    # allowed to create incident, and tried to create incident
    else:
        data = ""

    return data


def direct_message_handler(integration_context: dict, request_body: dict, conversation: dict, message: str):
    """
    Handles a direct message sent to the bot
    :param integration_context: Cached object to retrieve relevant data from
    :param request_body: Activity payload
    :param conversation: Conversation object sent
    :param message: The direct message sent
    :return: None
    """
    conversation_id: str = conversation.get("id", "")

    from_property: dict = request_body.get("from", {})
    user_id: str = from_property.get("id", "")

    team_member: dict = get_team_member(integration_context, user_id)
    if team_member:
        # enrich our data with the sender info
        demisto.debug(f"direct_message_handler for: {team_member=}")
        request_body["from"].update(team_member)

    username: str = team_member.get("username", "")
    user_email: str = team_member.get("user_email", "")
    user_upn = team_member.get("user_principal_name", "")
    demisto_user = demisto.findUser(email=user_email)
    if not demisto_user:
        demisto_user = demisto.findUser(username=username)
    if not demisto_user:
        demisto_user = demisto.findUser(email=user_upn)
    if not demisto_user:
        demisto.debug("direct_message_handler Failed to find user by email, username and UPN")

    formatted_message = ""

    attachment = {}

    return_card: bool = False

    allow_external_incidents_creation: bool = demisto.params().get("allow_external_incidents_creation", False)

    lowered_message = message.lower()
    # the command is to create new incident
    create_incident = "incident" in lowered_message and (
        "create" in lowered_message or "open" in lowered_message or "new" in lowered_message
    )
    data = (
        ""
        if demisto_user
        else handle_external_user(
            user_email or username,
            allow_external_incidents_creation,
            create_incident,
        )
    )
    # internal user or external who's trying to create incident
    if not data:
        if create_incident:
            data = process_incident_create_message(demisto_user, message, request_body)
            formatted_message = urlify_hyperlinks(data)
        else:  # internal user running any command except for new incident
            try:
                data = demisto.directMessage(message, username, user_email, allow_external_incidents_creation)
                return_card = True
                if data.startswith("`"):  # We got a list of incidents/tasks:
                    data_by_line: list = data.replace("```", "").strip().split("\n")
                    return_card = True
                    if data_by_line[0].startswith("Task"):
                        attachment = process_tasks_list(data_by_line)
                    else:
                        attachment = process_incidents_list(data_by_line)
                else:  # Mirror investigation command / unknown direct message
                    attachment = process_mirror_or_unknown_message(data)
            except Exception as e:
                data = str(e)

    if return_card:
        conversation = {"type": "message", "attachments": [attachment]}
    else:
        formatted_message = formatted_message or data
        conversation = {"type": "message", "text": formatted_message}

    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")

    send_message_request(service_url, conversation_id, conversation)


def entitlement_handler(integration_context: dict, request_body: dict, value: dict, conversation_id: str):
    """
    Handles activity the bot received as part of TeamsAsk flow, which includes entitlement
    :param integration_context: Cached object to retrieve relevant data from
    :param request_body: Activity payload
    :param value: Object which includes entitlement response data
    :param conversation_id: Message conversation ID
    :return: None
    """
    response: str = value.get("response", "")
    if not response:
        # Adaptive Card Response Received
        remove_keys = ["entitlement", "investigation_id", "task_id"]
        response_dict = {key: value for key, value in value.items() if key not in remove_keys}
        response = tableToMarkdown("Response", response_dict, headers=list(response_dict.keys()))

    demisto.debug(f"Entitlement Response Received\n{value}")
    entitlement_guid: str = value.get("entitlement", "")
    investigation_id: str = value.get("investigation_id", "")
    task_id: str = value.get("task_id", "")
    from_property: dict = request_body.get("from", {})
    team_members_id: str = from_property.get("id", "")

    # Try to get user email - check team cache first, then proactive users cache, then Graph API
    user_email = ""
    try:
        team_member: dict = get_team_member(integration_context, team_members_id)
        user_email = team_member.get("user_email", "")
        demisto.debug(f"Found user in team cache: {user_email}")
    except ValueError:
        demisto.debug("Cant Find user in team cache, check proactive users cache")
        # Load the proactive_users LRU cache (OrderedDict preserves insertion order)
        raw_proactive_users = integration_context.get("proactive_users", "{}")
        if isinstance(raw_proactive_users, str):
            raw_proactive_users = json.loads(raw_proactive_users)
        proactive_users: OrderedDict = OrderedDict(raw_proactive_users)

        cached_user = lru_cache_get(proactive_users, team_members_id)
        if cached_user:
            user_email = cached_user.get("email", "")
            demisto.debug(f"Found user in proactive users cache: {user_email}")
        else:
            # Fallback: use Graph API to get user details
            demisto.debug(f"User {team_members_id} not in proactive users cache, querying Graph API")
            try:
                user_data = get_user(team_members_id, select_fields="mail,userPrincipalName")
                if user_data and user_data[0]:
                    user_email = user_data[0].get("mail") or user_data[0].get("userPrincipalName", "")
                    demisto.debug(f"Retrieved user email from Graph API: {user_email}")

                    # Cache the user email using LRU eviction (max PROACTIVE_CACHE_MAX_SIZE entries)
                    if user_data[0].get("mail"):
                        lru_cache_set(proactive_users, team_members_id, {"email": user_email})
                        integration_context["proactive_users"] = json.dumps(proactive_users)
                        set_integration_context(integration_context)
                        demisto.debug(f"Cached user email in integration context: {team_members_id} -> {user_email}")
            except Exception as e:
                raise ValueError(f"Failed to retrieve user details: {str(e)}")

    demisto.handleEntitlementForUser(
        incidentID=investigation_id,
        guid=entitlement_guid,
        taskID=task_id,
        email=user_email,
        content=response,
    )
    activity_id: str = request_body.get("replyToId", "")
    demisto.debug(f"Request data: {request_body}")
    service_url: str = integration_context.get("service_url", "")
    if not service_url:
        raise ValueError("Did not find service URL. Try messaging the bot on Microsoft Teams")
    update_message(service_url, conversation_id, activity_id, "Your response was submitted successfully.")


def message_handler(integration_context: dict, request_body: dict, channel_data: dict, message: str):
    """
    Handles a message in which the bot was mentioned
    :param integration_context: Cached object to retrieve relevant data from
    :param request_body: Activity payload
    :param channel_data: Microsoft Teams tenant, team and channel details
    :param message: The message which was sent mentioning the bot
    :return: None
    """
    channel: dict = channel_data.get("channel", {})
    channel_id: str = channel.get("id", "")
    team_id: str = channel_data.get("team", {}).get("id", "")

    from_property: dict = request_body.get("from", {})
    team_member_id: str = from_property.get("id", "")

    if integration_context.get("teams"):
        teams: list = json.loads(integration_context["teams"])
        for team in teams:
            if team.get("team_id", "") == team_id:
                mirrored_channels: list = team.get("mirrored_channels", [])
                for mirrored_channel in mirrored_channels:
                    if mirrored_channel.get("channel_id") == channel_id:
                        if mirrored_channel.get("mirror_direction", "") != "FromDemisto" and "none" not in mirrored_channel.get(
                            "mirror_type", ""
                        ):
                            investigation_id: str = mirrored_channel.get("investigation_id", "")
                            username: str = from_property.get("name", "")
                            user_email: str = get_team_member(integration_context, team_member_id).get("user_email", "")
                            demisto.debug(f"Adding Entry {message} to investigation {investigation_id}")
                            demisto.addEntry(
                                id=investigation_id,
                                # when pasting the message into the chat, it contains leading and trailing whitespaces
                                entry=message.strip(),
                                username=username,
                                email=user_email,
                                footer=f"\n**{ENTRY_FOOTER}**",
                            )
                        return


@APP.route("/health", methods=["GET"])
def health_check():
    demisto.debug("Microsoft Teams Integration received a local health check")
    return Response("Microsoft Teams long running integration server is up.", status=200, mimetype="text/plain")


@APP.route("/", methods=["POST"])
def messages() -> Response:
    """
    Main handler for messages sent to the bot
    """
    try:
        demisto.debug("Microsoft Teams Integration received a message from Teams")
        demisto.debug("Processing POST query...")
        headers: dict = cast(dict[Any, Any], request.headers)

        if validate_auth_header(headers) is False:
            return Response(response="Authorization header validation failed - JWT validation error", status=401)
        else:
            request_body: dict = request.json  # type: ignore[assignment]
            integration_context: dict = get_integration_context()
            service_url: str = request_body.get("serviceUrl", "")
            if service_url:
                service_url = service_url[:-1] if service_url.endswith("/") else service_url
                integration_context["service_url"] = service_url
                set_integration_context(integration_context)

            channel_data: dict = request_body.get("channelData", {})
            event_type: str = channel_data.get("eventType", "")
            demisto.debug(f"Event Type is: {event_type}")

            conversation: dict = request_body.get("conversation", {})
            conversation_type: str = conversation.get("conversationType", "")
            conversation_id: str = conversation.get("id", "")
            demisto.debug(f"conversation type is: {conversation_type}")

            message_text: str = request_body.get("text", "")

            # Remove bot mention
            bot_name = integration_context.get("bot_name", "")
            formatted_message: str = message_text.replace(f"<at>{bot_name}</at>", "")

            value: dict = request_body.get("value", {})

            if event_type == "teamMemberAdded":
                demisto.info("New Microsoft Teams team member was added")
                member_added_handler(integration_context, request_body, channel_data)
                demisto.debug(
                    f'Updated team in the integration context. '
                    f'Current saved teams: {json.dumps(get_integration_context().get("teams"))}'
                )
            elif event_type == "teamRenamed":
                demisto.debug("Microsoft Teams team was renamed, updating teams cache")
                team_renamed_handler(integration_context, channel_data)
            elif event_type == "teamDeleted":
                demisto.debug("Microsoft Teams team was deleted, updating teams cache")
                team_deleted_handler(integration_context, channel_data)
            elif value:
                # In TeamsAsk process
                demisto.info("Got response from user in MicrosoftTeamsAsk process")
                entitlement_handler(integration_context, request_body, value, conversation_id)
            elif conversation_type == "personal":
                demisto.info("Got direct message to the bot")
                demisto.debug(f"Text is : {request_body.get('text')}")
                if request_body.get("membersAdded", []):
                    demisto.debug("the bot was added to a one-to-one chat")
                direct_message_handler(integration_context, request_body, conversation, formatted_message)
            else:
                demisto.info("Got message mentioning the bot")
                demisto.debug(f"the message is from: {request_body.get('from', {})}")
                message_handler(integration_context, request_body, channel_data, formatted_message)
        demisto.info("Finished processing Microsoft Teams activity successfully")
        demisto.updateModuleHealth("")
        return Response(status=200)
    except Exception as e:
        err_msg = f"Error occurred when handling incoming message {e!s}"
        demisto.error(err_msg)
        return Response(response=err_msg, status=400)


def ring_user_request(call_request_data):
    return http_request(
        method="POST",
        url=f"{GRAPH_BASE_URL}/v1.0/communications/calls",
        json_=call_request_data,
        auth_type=CLIENT_CREDENTIALS_FLOW,
    )


def ring_user():
    """Rings a user on Teams.

    Notes:
        This is a ring only! no media plays in case the generated call is answered.

    Returns:
        None.
    """

    bot_id = BOT_ID
    integration_context: dict = get_integration_context()
    tenant_id: str = integration_context.get("tenant_id", "")
    if not tenant_id:
        raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)
    # get user to call name and id
    username_to_call = demisto.args().get("username")
    user: list = get_user(username_to_call, auth_type=CLIENT_CREDENTIALS_FLOW)
    if not (user and user[0].get("id")):
        raise ValueError(f"User {username_to_call} was not found")

    call_request_data = {
        "@odata.type": "#microsoft.graph.call",
        "callbackUri": "https://callback.url",
        "direction": "outgoing",
        "source": {
            "@odata.type": "#microsoft.graph.participantInfo",
            "identity": {
                "@odata.type": "#microsoft.graph.identitySet",
                "application": {"@odata.type": "#microsoft.graph.identity", "id": bot_id},
            },
        },
        "targets": [
            {
                "@odata.type": "#microsoft.graph.invitationParticipantInfo",
                "identity": {
                    "@odata.type": "#microsoft.graph.identitySet",
                    "user": {
                        "@odata.type": "#microsoft.graph.identity",
                        "displayName": username_to_call,
                        "id": user[0].get("id"),
                    },
                },
            }
        ],
        "requestedModalities": ["audio"],
        "mediaConfig": {
            "@odata.type": "#microsoft.graph.serviceHostedMediaConfig",
        },
        "tenantId": tenant_id,
    }
    response = ring_user_request(call_request_data)

    return_outputs(f"Calling {username_to_call}", {}, response)


def update_integration_context_samples(incidents: list, max_samples: int = MAX_SAMPLES):
    """
    Updates the integration context samples with the newly created incident.
    If the size of the samples has reached `MAX_SAMPLES`, will pop out the latest sample.
    Args:
        incidents (list): The list of the newly created incidents.
        max_samples (int): Max samples size.
    """
    ctx = get_integration_context()
    updated_samples_list: list[dict] = incidents + ctx.get("samples", [])
    ctx["samples"] = updated_samples_list[:max_samples]
    set_integration_context(ctx)


def long_running_loop():
    """
    The infinite loop which runs the mirror loop and the bot app in two different threads
    """
    while True:
        certificate: str = CERTIFICATE
        private_key: str = PRIVATE_KEY

        certificate_path = ""
        private_key_path = ""

        server = None

        try:
            port_mapping: str = PARAMS.get("longRunningPort", "")
            port: int
            if port_mapping:
                port = int(port_mapping.split(":")[1]) if ":" in port_mapping else int(port_mapping)
            else:
                raise ValueError("No port mapping was provided")
            Thread(target=channel_mirror_loop, daemon=True).start()
            demisto.info("Started channel mirror loop thread")

            ssl_args = {}

            if certificate and private_key:
                certificate_file = NamedTemporaryFile(delete=False)
                certificate_path = certificate_file.name
                certificate_file.write(bytes(certificate, "utf-8"))
                certificate_file.close()

                private_key_file = NamedTemporaryFile(delete=False)
                private_key_path = private_key_file.name
                private_key_file.write(bytes(private_key, "utf-8"))
                private_key_file.close()

                context = SSLContext(PROTOCOL_TLSv1_2)
                context.load_cert_chain(certificate_path, private_key_path)
                ssl_args["ssl_context"] = context

                demisto.info("Starting HTTPS Server")
            else:
                demisto.info("Starting HTTP Server")

            server = WSGIServer(("0.0.0.0", port), APP, log=DEMISTO_LOGGER, error_log=ERROR_LOGGER, **ssl_args)
            demisto.updateModuleHealth("")
            server.serve_forever()
        except SSLError as e:
            ssl_err_message = f"Failed to validate certificate and/or private key: {e!s}"
            demisto.error(ssl_err_message)
            raise ValueError(ssl_err_message) from e
        except Exception as e:
            error_message = str(e)
            demisto.error(f"An error occurred in long running loop: {error_message} - {format_exc()}")
            demisto.updateModuleHealth(f"An error occurred: {error_message}")
        finally:
            if certificate_path:
                os.unlink(certificate_path)
            if private_key_path:
                os.unlink(private_key_path)
            if server:
                server.stop()
            time.sleep(5)


def get_token_permissions(access_token: str) -> list[str]:
    """
    Decodes the provided access token and retrieves a list of API permissions associated with the token.

    :param access_token: the access token to decode.
    :return: A list of the token's API permission roles.
    """
    decoded_token = jwt.decode(access_token, options={"verify_signature": False})

    if AUTH_TYPE == CLIENT_CREDENTIALS_FLOW:
        roles = decoded_token.get("roles", [])

    else:  # Authorization code flow
        roles = decoded_token.get("scp", "")
        roles = roles.split()

    return roles


def token_permissions_list_command():
    """
    Gets the Graph access token stored in the integration context and displays the token's API permissions in the war room.

    Use-case:
    This command is ideal for users encountering insufficient permissions errors when attempting to
    execute an integration command.
    By utilizing this command, the user can identify the current permissions associated with their token (app), compare them to
    the required permissions for executing the desired command (detailed in the integration's docs), and determine any additional
    permissions needed to be added to their application.
    """
    # Get the used token from the integration context:
    access_token: str = get_graph_access_token()

    # Decode the token and extract the roles:
    if access_token:
        roles = get_token_permissions(access_token)

        if roles:
            hr = tableToMarkdown(
                f"The current API permissions in the Teams application are: ({len(roles)})\n"
                f"Authorization type is: {AUTH_TYPE}",
                sorted(roles),
                headers=["Permission"],
            )
        else:
            hr = "No permissions obtained for the used graph access token."

    else:
        hr = "Graph access token is not set."

    demisto.debug(f"'microsoft-teams-token-permissions-list' command result is: {hr}. Authorization type is: {AUTH_TYPE}.")

    result = CommandResults(readable_output=hr)

    return_results(result)


def create_messaging_endpoint_command():
    """
    Generates the messaging endpoint, based on the server url, the server version and the instance configurations.

    The messaging endpoint should be added to the Demisto bot configuration in Microsoft Teams as part of the Prerequisites of
    the integration's set-up.
    Link to documentation: https://xsoar.pan.dev/docs/reference/integrations/microsoft-teams#1-using-cortex-xsoar-or-cortex-xsiam-rerouting
    """
    server_address = ""
    messaging_endpoint = ""

    # Get instance name and server url:
    urls = demisto.demistoUrls()
    instance_name = urllib.parse.quote(demisto.integrationInstance())
    xsoar_url = urls.get("server", "")
    engine_url = demisto.args().get("engine_url", "")

    if is_using_engine():  # In case of an XSOAR engine user - The user must provide the engine address.
        if not engine_url:
            raise ValueError(
                "Your instance configuration involves a Cortex XSOAR engine.\nIn that case the messaging endpoint "
                "that should be added to the Demisto bot configuration in Microsoft Teams is the engine's IP "
                "(or DNS name) and the port in use, in the following format - `https://IP:port` or `http://IP:port`."
                " For example - `https://my-engine.name:443`, `http://1.1.1.1:443`.\nTo test the format validity run"
                " this command with your engine's URL set as the value of the `engine_url` argument."
            )

        elif engine_url and not re.search(XSOAR_ENGINE_URL_REGEX, engine_url):  # engine url is not valid
            raise ValueError(
                "Invalid engine URL - Please ensure that the `engine_url` includes the IP (or DNS name)"
                " and the port in use, and that it is in the correct format: `https://IP:port` or `http://IP:port`."
            )
        else:
            messaging_endpoint = engine_url

    elif engine_url:  # engine_url was unnecessarily set
        raise ValueError(
            "Your instance configuration doesn't involve a Cortex XSOAR engine, but an `engine_url` was set.\n"
            "If you wish to run on an engine - set this option in the instance configuration. Otherwise, delete "
            "the value of the `engine_url` argument."
        )

    elif is_xsoar_on_prem():
        messaging_endpoint = urljoin(urljoin(xsoar_url, "instance/execute"), instance_name)

    else:  # XSIAM or XSOAR SAAS
        if is_xsiam() or is_platform():
            # Replace the 'xdr' with 'crtx' in the hostname of XSIAM tenants
            # This substitution is related to this platform ticket: https://jira-dc.paloaltonetworks.com/browse/CIAC-12256.
            xsoar_url = xsoar_url.replace("xdr", "crtx", 1)

        # Add the 'ext-' prefix to the xsoar url
        if xsoar_url.startswith("http://"):
            server_address = xsoar_url.replace("http://", "http://ext-", 1)
        elif xsoar_url.startswith("https://"):
            server_address = xsoar_url.replace("https://", "https://ext-", 1)

        messaging_endpoint = urljoin(urljoin(server_address, "xsoar/instance/execute"), instance_name)

    hr = (
        f"The messaging endpoint is:\n `{messaging_endpoint}`\n\n The messaging endpoint should be added to the Demisto bot"
        f" configuration in Microsoft Teams as part of the prerequisites of the integration's setup.\n"
        f"For more information see: [Integration Documentation](https://xsoar.pan.dev/docs/reference/integrations/microsoft-teams#create-the-demisto-bot-in-microsoft-teams)."
    )

    demisto.debug(
        f"The messaging endpoint that should be added to the Demisto bot configuration in Microsoft Teams is:"
        f" {messaging_endpoint}"
    )

    result = CommandResults(readable_output=hr)

    return_results(result)


def validate_auth_code_flow_params(command: str = ""):
    """
    Validates that the necessary parameters for the Authorization Code flow have been received.
    Raises a DemistoException if a required parameter is missing.
    :param command: the command that should be executed
    """
    if not all([AUTH_CODE, REDIRECT_URI, AUTH_TYPE == AUTHORIZATION_CODE_FLOW]):
        err = f"In order to use the '{command}' command, "
        if not AUTH_CODE and not REDIRECT_URI and AUTH_TYPE != AUTHORIZATION_CODE_FLOW:
            raise DemistoException(
                err + "Please set the necessary parameters for the Authorization Code flow in the integration configuration."
            )
        elif AUTH_TYPE != AUTHORIZATION_CODE_FLOW:
            raise DemistoException(
                err + "you must set the 'Authentication Type' parameter to 'Authorization Code' in "
                "the integration configuration."
            )
        else:  # not all([AUTH_CODE, REDIRECT_URI]):
            raise DemistoException(
                err + "you must provide both 'Application redirect URI' and 'Authorization code' in "
                "the integration configuration for the Authorization Code flow."
            )


def test_connection():
    """
    Test connectivity in the Authorization Code flow mode.
    """
    get_graph_access_token()  # If fails, get_graph_access_token returns an error
    return_results(CommandResults(readable_output="✅ Success!"))


def test_module():
    """Tests API connectivity and authentication for Bot Framework API only.
    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.
    :return: 'ok' if test passed.
    :rtype: ``str``
    """
    if not BOT_ID or not BOT_PASSWORD:
        raise DemistoException("Bot ID and Bot Password must be provided.")

    raise DemistoException(
        "Test module is unavailable for the Microsoft Teams Integration."
        " Please use the !microsoft-teams-integration-health command to test connectivity."
    )


def generate_login_url_command():
    tenant_id = get_integration_context().get("tenant_id")
    if not tenant_id:
        raise Exception(
            "Tenant ID is missing, please make sure that the messaging endpoint is configured correctly,"
            " and the bot is added to a team."
        )
    login_url = (
        f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize?"
        f"response_type=code&scope=offline_access%20https://graph.microsoft.com/.default"
        f"&client_id={BOT_ID}&redirect_uri={REDIRECT_URI}"
    )

    result_msg = f"""### Authorization instructions
1. Click on the [login URL]({login_url}) to sign in and grant Cortex XSOAR permissions for your Azure Service Management.
You will be automatically redirected to a link with the following structure:
```REDIRECT_URI?code=AUTH_CODE&session_state=SESSION_STATE```
2. Copy the `AUTH_CODE` (without the `code=` prefix, and the `session_state` parameter)
and paste it in your instance configuration under the **Authorization code** parameter.
    """
    return_results(CommandResults(readable_output=result_msg))


def fetch_samples():
    """
    The integration fetches incidents in the long-running-execution command. Fetch incidents is called
    only when "Pull From Instance" is clicked in create new classifier section in Cortex XSOAR.
    The fetch incidents returns samples of incidents generated by the long-running-execution.
    """
    demisto.incidents(get_integration_context().get("samples"))


def auth_type_switch_handling():
    """
    Handling cases where the user switches the auth type in the integration instance (from the client credentials flow to the
    auth code flow and vice versa), by auto-resetting the Graph API authorization in the integration context.
    """
    integration_context = get_integration_context()
    current_auth_type = integration_context.get("current_auth_type", "")
    if current_auth_type:
        demisto.debug(f"current_auth_type is: {current_auth_type}")
    else:
        # current_auth_type is not set - First run of the integration instance
        demisto.debug(
            f"This is the first run of the integration instance.\n"
            f"Setting the current_auth_type in the integration context to {AUTH_TYPE}."
        )
        integration_context["current_auth_type"] = AUTH_TYPE
        set_integration_context(integration_context)
        current_auth_type = AUTH_TYPE

    if current_auth_type != AUTH_TYPE:
        # First run after the user switched the authentication type
        demisto.debug(
            f"The user switched the instance authentication type from {current_auth_type} to {AUTH_TYPE}.\n"
            f"Resetting the integration context."
        )
        reset_auth()
        integration_context = get_integration_context()
        demisto.debug(f"Setting the current_auth_type in the integration context to {AUTH_TYPE}.")
        integration_context["current_auth_type"] = AUTH_TYPE
        set_integration_context(integration_context)


def expand_permission_list(permissions: list[str]) -> set[str]:
    """
    Takes the given permission list and extends it to include relevant lower privileged sub permissions
    that are supported by the given permissions. (e.g. if theres Chat.ReadWrite then Chat.Read is implicitly supported)
    :param permissions: Permissions list extracted from the cached graph token

    :return: Extended permissions list including lower privileged permissions supported by the original permissions.
    """
    expanded_permissions = set()
    for permission in permissions:
        expanded_permissions.add(permission)
        if permission in HIGHER_PERMISSIONS:
            expanded_permissions.update(HIGHER_PERMISSIONS[GraphPermissions(permission)])

    demisto.debug(f"Expanded token permission list: {expanded_permissions}")
    return expanded_permissions


def create_missing_permissions_section(missing_permissions: list[str], permission_type: str, auth_type: str = AUTH_TYPE) -> str:
    """
    Generates a detailed error message for the missing permissions.

    :param missing_permissions: List of missing permissions
    :param permission_type: The type of permissions ('Application' or 'Delegated')
    :param auth_type (str): Authentication type used in the request (Client credentials / Authorization code)
    :return: Error message string
    """
    error_message = f"""\
The current access token is missing the following permissions:
{permission_type} - {", ".join(missing_permissions)}

To resolve the issue, add the missing permissions to your bot in the Azure portal:
Click API permissions > Add a permission > Microsoft Graph > {permission_type} permissions, add the required missing permissions \
and grant admin consent.

Once done, reset your token using the "!microsoft-teams-auth-reset" command.
"""

    if auth_type == AUTHORIZATION_CODE_FLOW:
        error_message += 'Then, generate a new auth code using "!microsoft-teams-generate-login-url".\n'

    error_message += (
        'The "!microsoft-teams-token-permissions-list" command can be used to '
        "list the permissions of the currently used graph access token."
    )

    return error_message


def insufficient_permissions_error_handler(auth_type: str = AUTH_TYPE) -> str:
    """
    Retrieve the known permission requirements for the failed command and give a detailed error message
    indicating the missing permissions and how to resolve the issue.

    :param auth_type (str): Authentication type used in the request (Client credentials / Authorization code)
    :return: Error message string with instructions regarding the missing permissions
    """
    command: str = demisto.command()
    error_message = []
    demisto.debug(f"Authentication type is: {auth_type}")
    missing_permissions = []
    required_permissions_for_command: list = COMMANDS_REQUIRED_PERMISSIONS.get(auth_type, {}).get(command, [])
    permission_type = "Application" if auth_type == CLIENT_CREDENTIALS_FLOW else "Delegated"
    if not required_permissions_for_command:
        demisto.error(
            "Got an insufficient permissions error from Microsoft Graph. "
            f"Could not resolve permission requirements for {command=}"
        )
        return ""

    # Get current token permissions
    integration_context: dict = get_integration_context()
    token_params = json.loads(
        integration_context.get(CREDENTIALS_TOKEN_PARAMS, "{}")
        if auth_type == CLIENT_CREDENTIALS_FLOW
        else integration_context.get(AUTHCODE_TOKEN_PARAMS, "{}")
    )
    graph_access_token: str = token_params.get("graph_access_token", "")
    if not graph_access_token:
        demisto.error(
            f"Could not find a cached graph access token while trying to resolve permission requirements for {command=}"
        )
        return ""

    graph_token_api_permissions = get_token_permissions(graph_access_token)
    expanded_permissions = expand_permission_list(graph_token_api_permissions)

    missing_permissions = [
        permission for permission in required_permissions_for_command if permission not in expanded_permissions
    ]

    # Start building the error message
    error_message.append(f"The {command} command using the {auth_type} Flow, requires the following API permissions:")

    error_message.append(f'{permission_type} - {", ".join(required_permissions_for_command)}\n')

    if missing_permissions:
        error_message.append(create_missing_permissions_section(missing_permissions, permission_type))
    else:
        error_message.append("Your current graph access token appears to have all the required permissions.")
        error_message.append("The issue may be related to an API scope issue, or a change in the Microsoft Teams API.")

    return "\n".join(error_message)


def main():  # pragma: no cover
    """COMMANDS MANAGER / SWITCH PANEL"""
    demisto.debug("Main started...")
    commands: dict = {
        "test-module": test_module,
        "long-running-execution": long_running_loop,
        "send-notification": send_message,
        "teams-send-notification-quick-action": send_message,
        "mirror-investigation": mirror_investigation,
        "close-channel": close_channel,
        "microsoft-teams-integration-health": integration_health,
        "create-channel": create_channel_command,
        "add-user-to-channel": add_user_to_channel_command,
        "fetch-incidents": fetch_samples,
        # 'microsoft-teams-create-team': create_team,
        # 'microsoft-teams-send-file': send_file,
        "microsoft-teams-ring-user": ring_user,
        "microsoft-teams-create-channel": create_channel_command,
        "microsoft-teams-add-user-to-channel": add_user_to_channel_command,
        "microsoft-teams-create-meeting": create_meeting_command,
        "microsoft-teams-channel-user-list": channel_user_list_command,
        "microsoft-teams-user-remove-from-channel": user_remove_from_channel_command,
        "microsoft-teams-generate-login-url": generate_login_url_command,
        "microsoft-teams-auth-reset": reset_auth_command,
        "microsoft-teams-token-permissions-list": token_permissions_list_command,
        "microsoft-teams-create-messaging-endpoint": create_messaging_endpoint_command,
        "microsoft-teams-message-update": message_update_command,
        "microsoft-teams-list-messages": list_messages_command,
        "microsoft-teams-send-proactive-message": send_proactive_message_command,
    }

    commands_auth_code: dict = {
        "microsoft-teams-auth-test": test_connection,
        "microsoft-teams-chat-create": chat_create_command,
        "microsoft-teams-message-send-to-chat": message_send_to_chat_command,
        "microsoft-teams-chat-add-user": chat_add_user_command,
        "microsoft-teams-chat-list": chat_list_command,
        "microsoft-teams-chat-member-list": chat_member_list_command,
        "microsoft-teams-chat-message-list": chat_message_list_command,
        "microsoft-teams-chat-update": chat_update_command,
    }

    """ EXECUTION """
    command: str = demisto.command()
    if command != "test-module":  # skipping test-module since it doesn't have integration context
        auth_type_switch_handling()  # handles auth type switch cases

    try:
        support_multithreading()
        handle_proxy()
        LOG(f"Command being called is {command}")
        if command in commands:
            commands[command]()
        elif command in commands_auth_code:
            validate_auth_code_flow_params(command)  # raises error in case one of the required params is missing
            commands_auth_code[command]()
        else:
            raise NotImplementedError(f"command {command} is not implemented.")
    # Log exceptions
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


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