diff --git a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.py b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.py
index 944409c2fa18..b566799ff04c 100644
--- a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.py
+++ b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.py
@@ -218,6 +218,7 @@ class GraphPermissions(str, Enum):
"microsoft-teams-auth-test": [],
"microsoft-teams-auth-reset": [],
"microsoft-teams-token-permissions-list": [],
+ "microsoft-teams-send-proactive-message": [],
},
CLIENT_CREDENTIALS_FLOW: {
"send-notification": [Perms.GROUPMEMBER_READ_ALL, Perms.CHANNEL_READBASIC_ALL],
@@ -270,6 +271,7 @@ class GraphPermissions(str, Enum):
"microsoft-teams-auth-test": [],
"microsoft-teams-auth-reset": [],
"microsoft-teams-token-permissions-list": [],
+ "microsoft-teams-send-proactive-message": [],
},
}
HIGHER_PERMISSIONS: dict[GraphPermissions, list[GraphPermissions]] = {
@@ -918,6 +920,229 @@ def get_bot_access_token() -> str:
raise ValueError("Failed to get bot access token")
+def get_graph_token_for_proactive_messaging() -> str:
+ """
+ Retrieves Microsoft Graph API access token specifically for proactive messaging user resolution.
+ Uses a separate token with Graph API scope instead of Bot Framework scope.
+ :return: The Microsoft Graph API access token
+ """
+ integration_context: dict = get_integration_context()
+ access_token: str = integration_context.get("graph_proactive_token", "")
+ valid_until: int = integration_context.get("graph_proactive_valid_until", 0)
+
+ if access_token and valid_until and epoch_seconds() < valid_until:
+ demisto.debug("Using cached Graph API token for proactive messaging")
+ return access_token
+
+ tenant_id = integration_context.get("tenant_id")
+ if not tenant_id:
+ raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)
+
+ data: dict = {
+ "grant_type": "client_credentials",
+ "client_id": BOT_ID,
+ "client_secret": BOT_PASSWORD,
+ "scope": "https://graph.microsoft.com/.default",
+ }
+
+ url: str = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
+ demisto.debug(f"Requesting Graph API token for proactive messaging from tenant {tenant_id}")
+ response: requests.Response = requests.post(url, data=data, verify=USE_SSL, proxies=PROXIES)
+
+ if not response.ok:
+ error = error_parser(response)
+ raise ValueError(f"Failed to get Graph access token for proactive messaging [{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["graph_proactive_token"] = access_token
+ integration_context["graph_proactive_valid_until"] = time_now + expires_in
+ set_integration_context(integration_context)
+ demisto.debug("Successfully obtained Graph API token for proactive messaging")
+ return access_token
+ except ValueError:
+ raise ValueError("Failed to get Graph access token for proactive messaging")
+
+
+def resolve_user_id_for_proactive_message(user_identifier: str) -> str:
+ """
+ Resolves a user identifier (displayName, mail, or userPrincipalName) to the actual user ID.
+ Uses Microsoft Graph API to query users.
+
+ :param user_identifier: User identifier - can be displayName, mail, userPrincipalName, or user ID
+ :return: The resolved user ID
+ """
+ demisto.debug(f"Resolving user identifier: {user_identifier}")
+
+ # Get Graph API token for user resolution
+ access_token = get_graph_token_for_proactive_messaging()
+
+ headers: dict = {
+ "Authorization": f"Bearer {access_token}",
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ }
+
+ # Try direct user ID lookup first (if it looks like a GUID)
+ if re.match(r'^[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}$', user_identifier):
+ demisto.debug(f"User identifier appears to be a GUID, using directly: {user_identifier}")
+ return user_identifier
+
+ # Try to get user by exact match on userPrincipalName or mail
+ url = f"{GRAPH_BASE_URL}/v1.0/users/{urllib.parse.quote(user_identifier)}"
+ demisto.debug(f"Attempting direct user lookup: {url}")
+
+ try:
+ response: requests.Response = requests.get(url, headers=headers, verify=USE_SSL, proxies=PROXIES)
+ if response.ok:
+ user_data = response.json()
+ user_id = user_data.get("id", "")
+ if user_id:
+ demisto.debug(f"Found user by exact match: {user_id}")
+ return user_id
+ except Exception as e:
+ demisto.debug(f"Direct user lookup failed: {str(e)}")
+
+ # Try filter query for exact match on displayName, mail, or userPrincipalName
+ filter_query = (
+ f"displayName eq '{user_identifier}' or "
+ f"userPrincipalName eq '{user_identifier}' or "
+ f"mail eq '{user_identifier}'"
+ )
+ url = f"{GRAPH_BASE_URL}/v1.0/users?$filter={urllib.parse.quote(filter_query)}"
+ demisto.debug(f"Attempting exact match filter query: {filter_query}")
+
+ response = requests.get(url, headers=headers, verify=USE_SSL, proxies=PROXIES)
+
+ if not response.ok:
+ error = error_parser(response)
+ raise ValueError(f"Failed to resolve user ID [{response.status_code}] - {error}")
+
+ users = response.json().get("value", [])
+
+ 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{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_id: str = BOT_ID
+ 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("Service URL not found. Please ensure the bot has been added to a team and has received at least one message.")
+
+ if not tenant_id:
+ raise ValueError(MISS_CONFIGURATION_ERROR_MESSAGE)
+
+ # Check if we have a cached conversation for this user
+ cached_conversations: dict = integration_context.get("proactive_conversations", {})
+ if isinstance(cached_conversations, str):
+ cached_conversations = json.loads(cached_conversations)
+
+ if user_id in cached_conversations:
+ conversation_id = cached_conversations[user_id]
+ demisto.debug(f"Using cached conversation ID for user {user_id}: {conversation_id}")
+ return conversation_id
+
+ # Create new conversation
+ conversation: dict = {
+ "bot": {"id": f"28:{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", "")
+
+ if not conversation_id:
+ raise ValueError("Failed to create conversation: No conversation ID returned")
+
+ # Cache the conversation ID
+ 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.
+
+ :param conversation_id: The conversation ID
+ :param message: The text message to send (optional if adaptive_card is provided)
+ :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.")
+
+ # Build the conversation payload
+ conversation_payload: dict = {"type": "message"}
+
+ if adaptive_card:
+ # Handle raw adaptive card format
+ adaptive_card = handle_raw_adaptive_card(adaptive_card)
+ conversation_payload["attachments"] = [adaptive_card]
+ elif message:
+ conversation_payload["text"] = message
+ conversation_payload["entities"] = []
+ else:
+ raise ValueError("Either message or adaptive_card must be provided")
+
+ 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'
@@ -2627,6 +2852,72 @@ def message_update_command():
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: str = args.get('adaptive_card', '')
+
+ # Validate inputs - message and adaptive_card are both optional, but at least one must be provided
+ 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")
+
+ # Step 1: Resolve user identifier to user ID
+ demisto.debug(f"Resolving user identifier: {user_id_arg}")
+ user_id = resolve_user_id_for_proactive_message(user_id_arg)
+
+ # Step 2: Create or retrieve proactive conversation
+ demisto.debug(f"Creating/retrieving proactive conversation for user: {user_id}")
+ conversation_id = create_proactive_conversation(user_id)
+
+ # Step 3: Prepare message or adaptive card
+ adaptive_card_obj = None
+ if adaptive_card_arg:
+ try:
+ adaptive_card_obj = json.loads(adaptive_card_arg)
+ except json.JSONDecodeError:
+ raise ValueError("Invalid adaptive card JSON format")
+
+ # Step 4: Send the proactive message
+ 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_obj
+ )
+
+ # Prepare outputs
+ outputs = {
+ "conversationId": conversation_id,
+ "userId": user_id,
+ "activityId": response.get("id", ""),
+ "userIdentifier": user_id_arg
+ }
+
+ result = CommandResults(
+ outputs_prefix="MicrosoftTeams.ProactiveMessage",
+ 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
@@ -3741,6 +4032,7 @@ def main(): # pragma: no cover
"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 = {
diff --git a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.yml b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.yml
index 225aecdee4da..f9cb743d507e 100644
--- a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.yml
+++ b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/MicrosoftTeams.yml
@@ -929,6 +929,43 @@ script:
- contextPath: MicrosoftTeams.Message.ID
description: ID of the message sent.
type: String
+ - arguments:
+ - description: |-
+ The user identifier to send the message to. Can be one of the following:
+ - User ID (GUID): e.g., "3fa9f28b-eb0e-463a-ba7b-8089fe9991e2"
+ - Email address: e.g., "user@example.com"
+ - User Principal Name: e.g., "user@domain.onmicrosoft.com"
+ - Display Name: e.g., "John Doe"
+ name: user_id
+ required: true
+ - description: The text message to send to the user.
+ name: message
+ - description: The Microsoft Teams adaptive card to send (in JSON format).
+ name: adaptive_card
+ description: |-
+ 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.
+
+ The command will:
+ 1. Resolve the user identifier to the actual user ID using Microsoft Graph API
+ 2. Create or retrieve an existing conversation with the user
+ 3. Send the message or adaptive card via the Bot Framework API
+
+ Note: Either 'message' or 'adaptive_card' must be provided, but not both.
+ name: microsoft-teams-send-proactive-message
+ outputs:
+ - contextPath: MicrosoftTeams.ProactiveMessage.conversationId
+ description: The ID of the conversation created or used.
+ type: String
+ - contextPath: MicrosoftTeams.ProactiveMessage.userId
+ description: The resolved user ID.
+ type: String
+ - contextPath: MicrosoftTeams.ProactiveMessage.activityId
+ description: The ID of the sent message activity.
+ type: String
+ - contextPath: MicrosoftTeams.ProactiveMessage.userIdentifier
+ description: The original user identifier provided.
+ type: String
dockerimage: demisto/teams:1.0.0.6091526
longRunning: true
longRunningPort: true
diff --git a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/README.md b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/README.md
index faa2f0ab8179..59c935e1664b 100644
--- a/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/README.md
+++ b/Packs/MicrosoftTeams/Integrations/MicrosoftTeams/README.md
@@ -1514,3 +1514,54 @@ Notes:
| 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.
+
+The command will:
+1. Resolve the user identifier to the actual user ID using Microsoft Graph API
+2. Create or retrieve an existing conversation with the user
+3. Send the message or adaptive card via the Bot Framework API
+
+**Use Case Example**: A user attempts to download a malicious file. An XSOAR playbook is triggered by a security event, and as part of the automated response, the playbook uses this command to send an Adaptive Card to the user. The card explains that the file has been quarantined, provides a brief security tip, and asks for confirmation that the user did not intentionally download the file.
+
+##### Base Command
+
+`microsoft-teams-send-proactive-message`
+
+##### Required Permissions
+
+This command uses the Bot Framework API and does not require specific Graph API permissions beyond the basic bot setup.
+
+However, to resolve user identifiers, the bot requires:
+- `User.Read.All` - *Application* (for user resolution via Graph API)
+
+##### Input
+
+| **Argument Name** | **Description** | **Required** |
+| --- | --- | --- |
+| user_id | The user identifier to send the message to. Can be one of the following:
- User ID (GUID): e.g., "3fa9f28b-eb0e-463a-ba7b-8089fe9991e2"
- Email address: e.g., "user@example.com"
- User Principal Name: e.g., "user@domain.onmicrosoft.com"
- Display Name: e.g., "John Doe" | 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.ProactiveMessage.conversationId | String | The ID of the conversation created or used. |
+| MicrosoftTeams.ProactiveMessage.userId | String | The resolved user ID. |
+| MicrosoftTeams.ProactiveMessage.activityId | String | The ID of the sent message activity. |
+| MicrosoftTeams.ProactiveMessage.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.
diff --git a/Packs/MicrosoftTeams/ReleaseNotes/1_5_52.md b/Packs/MicrosoftTeams/ReleaseNotes/1_5_52.md
new file mode 100644
index 000000000000..70121c2b19e8
--- /dev/null
+++ b/Packs/MicrosoftTeams/ReleaseNotes/1_5_52.md
@@ -0,0 +1,5 @@
+#### Integrations
+
+##### Microsoft Teams
+
+- Added the **microsoft-teams-send-proactive-message** command to send proactive messages to any user across the organization without requiring them to be in a specific team or channel.
diff --git a/Packs/MicrosoftTeams/pack_metadata.json b/Packs/MicrosoftTeams/pack_metadata.json
index 10c298a3b825..964316b0046d 100644
--- a/Packs/MicrosoftTeams/pack_metadata.json
+++ b/Packs/MicrosoftTeams/pack_metadata.json
@@ -2,7 +2,7 @@
"name": "Microsoft Teams",
"description": "Send messages and notifications to your team members.",
"support": "xsoar",
- "currentVersion": "1.5.51",
+ "currentVersion": "1.5.52",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",