From 297e03d98f3e612ac142c539dead180a7dcc766c Mon Sep 17 00:00:00 2001 From: bb-splunk Date: Wed, 10 Apr 2024 18:14:09 +0200 Subject: [PATCH 01/10] SOARHELP-3212 Fixed error message handling --- README.md | 2 +- microsoftteams.json | 4 ++-- microsoftteams_connector.py | 10 +++++----- release_notes/2.5.2.md | 1 + 4 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 release_notes/2.5.2.md diff --git a/README.md b/README.md index 1cfa68b..f800185 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Microsoft Teams Publisher: Splunk -Connector Version: 2.5.1 +Connector Version: 2.5.2 Product Vendor: Microsoft Product Name: Teams Product Version Supported (regex): ".\*" diff --git a/microsoftteams.json b/microsoftteams.json index 4423a6f..a529107 100644 --- a/microsoftteams.json +++ b/microsoftteams.json @@ -10,8 +10,8 @@ "product_version_regex": ".*", "publisher": "Splunk", "license": "Copyright (c) 2019-2024 Splunk Inc.", - "app_version": "2.5.1", - "utctime_updated": "2024-04-04T10:47:00.000000Z", + "app_version": "2.5.2", + "utctime_updated": "2024-04-10T10:46:03.000000Z", "package_name": "phantom_microsoftteams", "main_module": "microsoftteams_connector.py", "min_phantom_version": "6.1.1", diff --git a/microsoftteams_connector.py b/microsoftteams_connector.py index 3e7f49d..385ba88 100644 --- a/microsoftteams_connector.py +++ b/microsoftteams_connector.py @@ -61,7 +61,7 @@ def _handle_login_redirect(request, key): return response -def _get_error_message_from_exception(self, e): +def _get_error_message_from_exception(e, app_connector): """ Get appropriate error message from the exception. :param e: Exception object @@ -71,7 +71,7 @@ def _get_error_message_from_exception(self, e): error_code = None error_msg = ERROR_MSG_UNAVAILABLE - self.error_print("Error occurred.", e) + app_connector.error_print("Error occurred.", e) try: if hasattr(e, "args"): @@ -81,7 +81,7 @@ def _get_error_message_from_exception(self, e): elif len(e.args) == 1: error_msg = e.args[0] except Exception as e: - self.error_print("Error occurred while fetching exception information. Details: {}".format(str(e))) + app_connector.error_print("Error occurred while fetching exception information. Details: {}".format(str(e))) if not error_code: error_text = "Error Message: {}".format(error_msg) @@ -1249,14 +1249,14 @@ def finalize(self): if self._state.get(MSTEAMS_TOKEN_STRING, {}).get(MSTEAMS_ACCESS_TOKEN_STRING): self._state[MSTEAMS_TOKEN_STRING][MSTEAMS_ACCESS_TOKEN_STRING] = self.encrypt_state(self._access_token, "access") except Exception as e: - self.debug_print("{}: {}".format(MSTEAMS_ENCRYPTION_ERROR, self._get_error_message_from_exception(e))) + self.debug_print("{}: {}".format(MSTEAMS_ENCRYPTION_ERROR, _get_error_message_from_exception(e, self))) return self.set_status(phantom.APP_ERROR, MSTEAMS_ENCRYPTION_ERROR) try: if self._state.get(MSTEAMS_TOKEN_STRING, {}).get(MSTEAMS_REFRESH_TOKEN_STRING): self._state[MSTEAMS_TOKEN_STRING][MSTEAMS_REFRESH_TOKEN_STRING] = self.encrypt_state(self._refresh_token, "refresh") except Exception as e: - self.debug_print("{}: {}".format(MSTEAMS_ENCRYPTION_ERROR, self._get_error_message_from_exception(e))) + self.debug_print("{}: {}".format(MSTEAMS_ENCRYPTION_ERROR, self._get_error_message_from_exception(e, self))) return self.set_status(phantom.APP_ERROR, MSTEAMS_ENCRYPTION_ERROR) self._state[MSTEAMS_STATE_IS_ENCRYPTED] = True # Save the state, this data is saved across actions and app upgrades diff --git a/release_notes/2.5.2.md b/release_notes/2.5.2.md new file mode 100644 index 0000000..de7f2b8 --- /dev/null +++ b/release_notes/2.5.2.md @@ -0,0 +1 @@ +* Fixed error message handling function \ No newline at end of file From d3a795f4d2c882b3085f42477ecbd8022c050bde Mon Sep 17 00:00:00 2001 From: root Date: Wed, 10 Apr 2024 09:34:44 -0700 Subject: [PATCH 02/10] Release notes for version 2.5.2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f800185..a2b8cd0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Microsoft Teams Publisher: Splunk -Connector Version: 2.5.2 +Connector Version: 2.5.2 Product Vendor: Microsoft Product Name: Teams Product Version Supported (regex): ".\*" From 334d3756a72c8d3af7ed453eace5729fbb0a52c7 Mon Sep 17 00:00:00 2001 From: grokas Date: Mon, 21 Oct 2024 16:01:32 -0400 Subject: [PATCH 03/10] PAPP-34866 added new teams chat actions --- manual_readme_content.md | 1 + microsoftteams.json | 275 ++++++++++++++++++++++++++++++++++++ microsoftteams_connector.py | 173 +++++++++++++++++++++++ microsoftteams_consts.py | 8 ++ 4 files changed, 457 insertions(+) diff --git a/manual_readme_content.md b/manual_readme_content.md index 8f3a2b9..ebcbcdb 100644 --- a/manual_readme_content.md +++ b/manual_readme_content.md @@ -55,6 +55,7 @@ This app requires creating an app in the Azure Active Directory. | Channel.ReadBasic.All | list channels | Read channel names and channel descriptions, on behalf of the signed-in user. | No | ChannelMessage.Send | send message | Allows an app to send channel messages in Microsoft Teams, on behalf of the signed-in user. | No | GroupMember.Read.All | list groups, list teams | Allows the app to list groups, read basic group properties and read membership of all groups the signed-in user has access to. | Yes + | Chat.ReadWrite | read and send chat messages | Allows the app to read and send messages in chats on behalf of the signed-in user. | No | After making these changes, click **Add permissions** at the bottom of the screen, then diff --git a/microsoftteams.json b/microsoftteams.json index a529107..d7d3a44 100644 --- a/microsoftteams.json +++ b/microsoftteams.json @@ -726,6 +726,281 @@ }, "versions": "EQ(*)" }, + { + "action": "list chats", + "description": "List chats for the authenticated user", + "type": "investigate", + "identifier": "list_chats", + "read_only": true, + "parameters": { + "user": { + "description": "Filter chats containing a specific user (by email or user id)", + "data_type": "string", + "required": false, + "primary": false, + "order": 1 + }, + "chat_type": { + "description": "Filter chats by type (e.g., oneOnOne, group, meeting)", + "data_type": "string", + "required": false, + "primary": false, + "value_list": ["oneOnOne", "group", "meeting"], + "order": 2 + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.user_id", + "data_type": "string", + "contains": [ + "ms teams user id" + ] + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "contains": [ + "ms teams chat id" + ], + "column_name": "Chat ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.topic", + "data_type": "string", + "column_name": "Topic", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.createdDateTime", + "data_type": "string", + "column_name": "Created On", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.lastUpdatedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.chatType", + "data_type": "string", + "column_name": "Chat Type", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.webUrl", + "data_type": "string", + "contains": [ + "url" + ] + }, + { + "data_path": "action_result.data.*.tenantId", + "data_type": "string" + }, + { + "data_path": "action_result.summary.total_chats", + "data_type": "numeric" + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "send direct message", + "description": "Send a direct message to a user", + "type": "generic", + "identifier": "send_direct_message", + "read_only": false, + "parameters": { + "user_id": { + "description": "ID of the user to send a direct message to", + "data_type": "string", + "required": true, + "primary": true, + "contains": ["ms teams user id"] + }, + "message": { + "description": "Message content to send", + "data_type": "string", + "required": true + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.user_id", + "data_type": "string", + "contains": ["ms teams user id"] + }, + { + "data_path": "action_result.parameter.message", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "contains": ["ms teams message id"] + }, + { + "data_path": "action_result.data.*.createdDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.from.user.id", + "data_type": "string", + "contains": ["ms teams user id"] + }, + { + "data_path": "action_result.data.*.from.user.displayName", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.body.content", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.body.contentType", + "data_type": "string" + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "send chat message", + "description": "Send a message to a specific chat", + "type": "generic", + "identifier": "send_chat_message", + "read_only": false, + "parameters": { + "chat_id": { + "description": "ID of the chat to send the message to", + "data_type": "string", + "required": true, + "primary": true, + "contains": ["ms teams chat id"] + }, + "message": { + "description": "Message content to send", + "data_type": "string", + "required": true + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.chat_id", + "data_type": "string", + "contains": ["ms teams chat id"] + }, + { + "data_path": "action_result.parameter.message", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "contains": ["ms teams message id"] + }, + { + "data_path": "action_result.data.*.createdDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.from.user.id", + "data_type": "string", + "contains": ["ms teams user id"] + }, + { + "data_path": "action_result.data.*.from.user.displayName", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.body.content", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.body.contentType", + "data_type": "string" + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, { "action": "list channels", "description": "Lists all channels of a group", diff --git a/microsoftteams_connector.py b/microsoftteams_connector.py index 385ba88..1abadfe 100644 --- a/microsoftteams_connector.py +++ b/microsoftteams_connector.py @@ -1161,6 +1161,176 @@ def _handle_create_meeting(self, param): return action_result.set_status(phantom.APP_SUCCESS, status_message='Meeting Created Successfully') + def _handle_list_chats(self, param): + """ This function is used to list all chats for the current user with optional filters. + + :param param: Dictionary of input parameters + :return: status success/failure + """ + + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) + action_result = self.add_action_result(ActionResult(dict(param))) + + user_filter = param.get(MSTEAMS_JSON_USER_FILTER) + chat_type_filter = param.get(MSTEAMS_JSON_CHAT_TYPE_FILTER) + + endpoint = MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT + + while True: + # make rest call + status, response = self._update_request(endpoint=endpoint, action_result=action_result) + + if phantom.is_fail(status): + return action_result.get_status() + + for chat in response.get('value', []): + # Filters + if chat_type_filter and chat_type_filter != chat.get('chatType', ''): + continue + + if user_filter: + user_match = False + for member in chat.get('members', []): + user_id = member.get('userId', '') + email = member.get('email', '') + if user_filter in user_id or user_filter in email: + user_match = True + break + if not user_match: + continue + + action_result.add_data(chat) + + if not response.get(MSTEAMS_NEXT_LINK_STRING): + break + + endpoint = response[MSTEAMS_NEXT_LINK_STRING] + + summary = action_result.update_summary({}) + summary['total_chats'] = action_result.get_data_size() + + return action_result.set_status(phantom.APP_SUCCESS) + + def _send_chat_message(self, action_result, chat_id, message): + """ This function is used to send a message to a chat. + + :param action_result: Object of ActionResult class + :param chat_id: ID of the chat + :param message: Message to be sent + :return: status success/failure, response + """ + endpoint = f'/chats/{chat_id}/messages' + + data = { + "body": { + "contentType": "html", + "content": message + } + } + + # make rest call + status, response = self._update_request(endpoint=endpoint, action_result=action_result, method='post', + data=json.dumps(data)) + + if phantom.is_fail(status): + return action_result.get_status(), None + + return phantom.APP_SUCCESS, response + + def _handle_send_chat_message(self, param): + """ This function is used to send a message to a chat. + + :param param: Dictionary of input parameters + :return: status success/failure + """ + + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) + action_result = self.add_action_result(ActionResult(dict(param))) + + chat_id = param[MSTEAMS_JSON_CHAT_ID] + message = param[MSTEAMS_JSON_MSG] + + status, response = self._send_chat_message(action_result, chat_id, message) + + if phantom.is_fail(status): + return action_result.get_status() + + action_result.add_data(response) + + return action_result.set_status(phantom.APP_SUCCESS, status_message='Message sent to chat successfully') + + def _handle_send_direct_message(self, param): + """ This function is used to send a direct message to a user. + + :param param: Dictionary of input parameters + :return: status success/failure + """ + + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) + action_result = self.add_action_result(ActionResult(dict(param))) + + user_id = param[MSTEAMS_JSON_USER_ID] + message = param[MSTEAMS_JSON_MSG] + + # Get our ID + status, me_response = self._update_request(endpoint=MSTEAMS_MSGRAPH_LIST_ME_ENDPOINT, action_result=action_result) + + if phantom.is_fail(status): + return action_result.set_status(phantom.APP_ERROR, "Failed to retrieve current user information") + + current_user_id = me_response.get('id') + if not current_user_id: + return action_result.set_status(phantom.APP_ERROR, "Failed to retrieve current user ID") + + # Get chats and find our 1:1 with user + status, response = self._update_request(endpoint=MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT, action_result=action_result) + + if phantom.is_fail(status): + return action_result.get_status() + + chat_id = None + for chat in response.get('value', []): + if chat.get('chatType') == 'oneOnOne': + members = chat.get('members', []) + if len(members) == 2 and any(member.get('userId') == user_id for member in members): + chat_id = chat.get('id') + break + + if not chat_id: + # Create new chat if none exists + create_chat_endpoint = '/chats' + create_chat_data = { + "chatType": "oneOnOne", + "members": [ + { + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{current_user_id}')" + }, + { + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')" + } + ] + } + status, response = self._update_request(endpoint=create_chat_endpoint, action_result=action_result, method='post', data=json.dumps(create_chat_data)) + + if phantom.is_fail(status): + return action_result.get_status() + + chat_id = response.get('id') + + # Send chat message now + status, response = self._send_chat_message(action_result, chat_id, message) + + if phantom.is_fail(status): + return action_result.get_status() + + action_result.add_data(response) + + return action_result.set_status(phantom.APP_SUCCESS, status_message='Message sent to user successfully') + def handle_action(self, param): """ This function gets current action identifier and calls member function of its own to handle the action. @@ -1174,10 +1344,13 @@ def handle_action(self, param): action_mapping = { 'test_connectivity': self._handle_test_connectivity, 'send_message': self._handle_send_message, + 'send_direct_message': self._handle_send_direct_message, + 'send_chat_message': self._handle_send_chat_message, 'list_groups': self._handle_list_groups, 'list_teams': self._handle_list_teams, 'list_users': self._handle_list_users, 'list_channels': self._handle_list_channels, + 'list_chats': self._handle_list_chats, 'get_admin_consent': self._handle_get_admin_consent, 'create_meeting': self._handle_create_meeting } diff --git a/microsoftteams_consts.py b/microsoftteams_consts.py index 26c12d2..7e043fb 100644 --- a/microsoftteams_consts.py +++ b/microsoftteams_consts.py @@ -27,6 +27,10 @@ MSTEAMS_MSGRAPH_LIST_USERS_ENDPOINT = '/users' MSTEAMS_MSGRAPH_LIST_CHANNELS_ENDPOINT = '/teams/{group_id}/channels' MSTEAMS_MSGRAPH_SEND_MSG_ENDPOINT = '/teams/{group_id}/channels/{channel_id}/messages' +MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT = '/me/chats' +MSTEAMS_MSGRAPH_LIST_ME_ENDPOINT = '/me' +MSTEAMS_MSGRAPH_LIST_USER_CHATS_ENDPOINT = '/users/{user_id}/chats' +MSTEAMS_MSGRAPH_SEND_DIRECT_MSG_ENDPOINT = '/chats/{chat_id}/messages' MSTEAMS_MSGRAPH_CALENDER_EVENT_ENDPOINT = '/me/calendar/events' MSTEAMS_MSGRAPH_ONLINE_MEETING_ENDPOINT = '/me/onlineMeetings' MSTEAMS_TC_FILE = 'oauth_task.out' @@ -60,6 +64,10 @@ "Resetting the state file with the default format. Please test the connectivity." MSTEAMS_JSON_GROUP_ID = 'group_id' MSTEAMS_JSON_CHANNEL_ID = 'channel_id' +MSTEAMS_JSON_CHAT_ID = 'chat_id' +MSTEAMS_JSON_USER_ID = 'user_id' +MSTEAMS_JSON_USER_FILTER = 'user' +MSTEAMS_JSON_CHAT_TYPE_FILTER = 'chat_type' MSTEAMS_JSON_MSG = 'message' MSTEAMS_JSON_SUBJECT = 'subject' MSTEAMS_JSON_CALENDAR = 'add_calendar_event' From 8406554a0b3a45c91561c4ac146b8902af932a14 Mon Sep 17 00:00:00 2001 From: grokas Date: Tue, 22 Oct 2024 17:33:59 -0400 Subject: [PATCH 04/10] PAPP-34866 testing updates --- microsoftteams.json | 262 +++++++++++++++++++++++++++++++++--- microsoftteams_connector.py | 3 + microsoftteams_consts.py | 2 + 3 files changed, 251 insertions(+), 16 deletions(-) diff --git a/microsoftteams.json b/microsoftteams.json index d7d3a44..165d028 100644 --- a/microsoftteams.json +++ b/microsoftteams.json @@ -736,17 +736,13 @@ "user": { "description": "Filter chats containing a specific user (by email or user id)", "data_type": "string", - "required": false, - "primary": false, - "order": 1 + "order": 0 }, "chat_type": { "description": "Filter chats by type (e.g., oneOnOne, group, meeting)", "data_type": "string", - "required": false, - "primary": false, - "value_list": ["oneOnOne", "group", "meeting"], - "order": 2 + "value_list": ["oneOnOne", "group", "meeting", "unknownFutureValue"], + "order": 1 } }, "output": [ @@ -807,6 +803,44 @@ "data_path": "action_result.data.*.tenantId", "data_type": "string" }, + { + "data_path": "action_result.data.*.viewpoint.isHidden", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.viewpoint.lastMessageReadDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.joinWebUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.conferenceId", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.joinUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.phones", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.quickDial", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.tollFreeNumbers", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.onlineMeetingInfo.tollNumber", + "data_type": "string" + }, { "data_path": "action_result.summary.total_chats", "data_type": "numeric" @@ -841,12 +875,14 @@ "data_type": "string", "required": true, "primary": true, - "contains": ["ms teams user id"] + "contains": ["ms teams user id"], + "order": 0 }, "message": { "description": "Message content to send", "data_type": "string", - "required": true + "required": true, + "order": 1 } }, "output": [ @@ -870,12 +906,65 @@ { "data_path": "action_result.data.*.id", "data_type": "string", - "contains": ["ms teams message id"] + "contains": ["ms teams message id"], + "column_name": "Message ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.chatId", + "data_type": "string", + "contains": ["ms teams chat id"] }, { "data_path": "action_result.data.*.createdDateTime", "data_type": "string" }, + { + "data_path": "action_result.data.*.deletedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.lastModifiedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.lastEditedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.etag", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.importance", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.locale", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.messageType", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.replyToId", + "data_type": "string", + "contains": ["ms teams message id"] + }, + { + "data_path": "action_result.data.*.subject", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.summary", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.webUrl", + "data_type": "string", + "contains": ["url"] + }, { "data_path": "action_result.data.*.from.user.id", "data_type": "string", @@ -887,12 +976,55 @@ }, { "data_path": "action_result.data.*.body.content", - "data_type": "string" + "data_type": "string", + "column_name": "Message", + "column_order": 1 }, { "data_path": "action_result.data.*.body.contentType", "data_type": "string" }, + { + "data_path": "action_result.data.*.attachments.*.content", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.contentType", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.contentUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.attachments.*.id", + "data_type": "string", + "contains": ["ms teams attachment id"] + }, + { + "data_path": "action_result.data.*.attachments.*.name", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.teamsAppId", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.thumbnailUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.channelIdentity.channelId", + "data_type": "string", + "contains": ["ms teams channel id"] + }, + { + "data_path": "action_result.data.*.channelIdentity.teamId", + "data_type": "string", + "contains": ["ms teams team id"] + }, { "data_path": "action_result.summary", "data_type": "string" @@ -927,12 +1059,14 @@ "data_type": "string", "required": true, "primary": true, - "contains": ["ms teams chat id"] + "contains": ["ms teams chat id"], + "order": 0 }, "message": { "description": "Message content to send", "data_type": "string", - "required": true + "required": true, + "order": 1 } }, "output": [ @@ -956,12 +1090,65 @@ { "data_path": "action_result.data.*.id", "data_type": "string", - "contains": ["ms teams message id"] + "contains": ["ms teams message id"], + "column_name": "Message ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.chatId", + "data_type": "string", + "contains": ["ms teams chat id"] }, { "data_path": "action_result.data.*.createdDateTime", "data_type": "string" }, + { + "data_path": "action_result.data.*.deletedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.lastModifiedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.lastEditedDateTime", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.etag", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.importance", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.locale", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.messageType", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.replyToId", + "data_type": "string", + "contains": ["ms teams message id"] + }, + { + "data_path": "action_result.data.*.subject", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.summary", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.webUrl", + "data_type": "string", + "contains": ["url"] + }, { "data_path": "action_result.data.*.from.user.id", "data_type": "string", @@ -973,12 +1160,55 @@ }, { "data_path": "action_result.data.*.body.content", - "data_type": "string" + "data_type": "string", + "column_name": "Message", + "column_order": 1 }, { "data_path": "action_result.data.*.body.contentType", "data_type": "string" }, + { + "data_path": "action_result.data.*.attachments.*.content", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.contentType", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.contentUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.attachments.*.id", + "data_type": "string", + "contains": ["ms teams attachment id"] + }, + { + "data_path": "action_result.data.*.attachments.*.name", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.teamsAppId", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.attachments.*.thumbnailUrl", + "data_type": "string", + "contains": ["url"] + }, + { + "data_path": "action_result.data.*.channelIdentity.channelId", + "data_type": "string", + "contains": ["ms teams channel id"] + }, + { + "data_path": "action_result.data.*.channelIdentity.teamId", + "data_type": "string", + "contains": ["ms teams team id"] + }, { "data_path": "action_result.summary", "data_type": "string" @@ -1794,7 +2024,7 @@ "data_path": "action_result.data.*.bodyPreview", "data_type": "string", "example_values": [ - ".........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtYT" + ".........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtY" ] }, { diff --git a/microsoftteams_connector.py b/microsoftteams_connector.py index 1abadfe..a45e19e 100644 --- a/microsoftteams_connector.py +++ b/microsoftteams_connector.py @@ -1174,6 +1174,9 @@ def _handle_list_chats(self, param): user_filter = param.get(MSTEAMS_JSON_USER_FILTER) chat_type_filter = param.get(MSTEAMS_JSON_CHAT_TYPE_FILTER) + if chat_type_filter and chat_type_filter not in MSTEAMS_VALID_CHAT_TYPES: + return action_result.set_status(phantom.APP_ERROR, "Invalid chat type filter") + endpoint = MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT while True: diff --git a/microsoftteams_consts.py b/microsoftteams_consts.py index 7e043fb..153b0be 100644 --- a/microsoftteams_consts.py +++ b/microsoftteams_consts.py @@ -87,6 +87,8 @@ MSTEAMS_NEXT_LINK_STRING = '@odata.nextLink' MSTEAMS_DEFAULT_TIMEOUT = 30 +MSTEAMS_VALID_CHAT_TYPES = ["oneOnOne", "group", "meeting", "unknownFutureValue"] + # For encryption and decryption MSTEAMS_ENCRYPT_TOKEN = "Encrypting the {} token" MSTEAMS_DECRYPT_TOKEN = "Decrypting the {} token" From b17b8cac09a81bfe2ebc408eeb9bcaf526b34846 Mon Sep 17 00:00:00 2001 From: grokas Date: Tue, 22 Oct 2024 17:34:42 -0400 Subject: [PATCH 05/10] PAPP-34866 formatting --- microsoftteams.json | 11 +- microsoftteams_connector.py | 584 +++++++++++++++++------------------- microsoftteams_consts.py | 145 ++++----- microsoftteams_view.py | 20 +- 4 files changed, 356 insertions(+), 404 deletions(-) diff --git a/microsoftteams.json b/microsoftteams.json index 165d028..f8d9562 100644 --- a/microsoftteams.json +++ b/microsoftteams.json @@ -10,11 +10,11 @@ "product_version_regex": ".*", "publisher": "Splunk", "license": "Copyright (c) 2019-2024 Splunk Inc.", - "app_version": "2.5.2", + "app_version": "2.6.2", "utctime_updated": "2024-04-10T10:46:03.000000Z", "package_name": "phantom_microsoftteams", "main_module": "microsoftteams_connector.py", - "min_phantom_version": "6.1.1", + "min_phantom_version": "6.2.2", "app_wizard_version": "1.0.0", "rest_handler": "microsoftteams_connector._handle_rest_request", "python_version": "3", @@ -755,11 +755,8 @@ ] }, { - "data_path": "action_result.parameter.user_id", - "data_type": "string", - "contains": [ - "ms teams user id" - ] + "data_path": "action_result.parameter.user", + "data_type": "string" }, { "data_path": "action_result.data.*.id", diff --git a/microsoftteams_connector.py b/microsoftteams_connector.py index a45e19e..bc15cce 100644 --- a/microsoftteams_connector.py +++ b/microsoftteams_connector.py @@ -40,24 +40,24 @@ def _handle_login_redirect(request, key): - """ This function is used to redirect login request to microsoft login page. + """This function is used to redirect login request to microsoft login page. :param request: Data given to REST endpoint :param key: Key to search in state file :return: response authorization_url/admin_consent_url """ - asset_id = request.GET.get('asset_id') + asset_id = request.GET.get("asset_id") if not asset_id: - return HttpResponse('ERROR: Asset ID not found in URL', content_type="text/plain", status=400) + return HttpResponse("ERROR: Asset ID not found in URL", content_type="text/plain", status=400) state = _load_app_state(asset_id) if not state: - return HttpResponse('ERROR: Invalid asset_id', content_type="text/plain", status=400) + return HttpResponse("ERROR: Invalid asset_id", content_type="text/plain", status=400) url = state.get(key) if not url: - return HttpResponse('App state is invalid, {key} not found.'.format(key=key), content_type="text/plain", status=400) + return HttpResponse("App state is invalid, {key} not found.".format(key=key), content_type="text/plain", status=400) response = HttpResponse(status=302) - response['Location'] = url + response["Location"] = url return response @@ -92,7 +92,7 @@ def _get_error_message_from_exception(e, app_connector): def _load_app_state(asset_id, app_connector=None): - """ This function is used to load the current state file. + """This function is used to load the current state file. :param asset_id: asset_id :param app_connector: Object of app_connector class @@ -102,35 +102,35 @@ def _load_app_state(asset_id, app_connector=None): asset_id = str(asset_id) if not asset_id or not asset_id.isalnum(): if app_connector: - app_connector.debug_print('In _load_app_state: Invalid asset_id') + app_connector.debug_print("In _load_app_state: Invalid asset_id") return {} app_dir = os.path.dirname(os.path.abspath(__file__)) - state_file = '{0}/{1}_state.json'.format(app_dir, asset_id) + state_file = "{0}/{1}_state.json".format(app_dir, asset_id) real_state_file_path = os.path.abspath(state_file) if not os.path.dirname(real_state_file_path) == app_dir: if app_connector: - app_connector.debug_print('In _load_app_state: Invalid asset_id') + app_connector.debug_print("In _load_app_state: Invalid asset_id") return {} state = {} try: - with open(real_state_file_path, 'r') as state_file_obj: + with open(real_state_file_path, "r") as state_file_obj: state_file_data = state_file_obj.read() state = json.loads(state_file_data) except Exception as e: if app_connector: error_text = _get_error_message_from_exception(e, app_connector) - app_connector.debug_print('In _load_app_state: {}'.format(error_text)) + app_connector.debug_print("In _load_app_state: {}".format(error_text)) if app_connector: - app_connector.debug_print('Loaded state: ', state) + app_connector.debug_print("Loaded state: ", state) return state def _save_app_state(state, asset_id, app_connector=None): - """ This function is used to save current state in file. + """This function is used to save current state in file. :param state: Dictionary which contains data to write in state file :param asset_id: asset_id @@ -141,94 +141,94 @@ def _save_app_state(state, asset_id, app_connector=None): asset_id = str(asset_id) if not asset_id or not asset_id.isalnum(): if app_connector: - app_connector.debug_print('In _save_app_state: Invalid asset_id') + app_connector.debug_print("In _save_app_state: Invalid asset_id") return {} app_dir = os.path.split(__file__)[0] - state_file = '{0}/{1}_state.json'.format(app_dir, asset_id) + state_file = "{0}/{1}_state.json".format(app_dir, asset_id) real_state_file_path = os.path.abspath(state_file) if not os.path.dirname(real_state_file_path) == app_dir: if app_connector: - app_connector.debug_print('In _save_app_state: Invalid asset_id') + app_connector.debug_print("In _save_app_state: Invalid asset_id") return {} if app_connector: - app_connector.debug_print('Saving state: ', state) + app_connector.debug_print("Saving state: ", state) try: - with open(real_state_file_path, 'w+') as state_file_obj: + with open(real_state_file_path, "w+") as state_file_obj: state_file_obj.write(json.dumps(state)) except Exception as e: error_text = _get_error_message_from_exception(e, app_connector) if app_connector: - app_connector.debug_print('Unable to save state file: {}'.format(error_text)) - print('Unable to save state file: {}'.format(error_text)) + app_connector.debug_print("Unable to save state file: {}".format(error_text)) + print("Unable to save state file: {}".format(error_text)) return phantom.APP_ERROR return phantom.APP_SUCCESS def _handle_login_response(request): - """ This function is used to get the login response of authorization request from microsoft login page. + """This function is used to get the login response of authorization request from microsoft login page. :param request: Data given to REST endpoint :return: HttpResponse. The response displayed on authorization URL page """ - asset_id = request.GET.get('state') + asset_id = request.GET.get("state") if not asset_id: - return HttpResponse('ERROR: Asset ID not found in URL\n{}'.format(json.dumps(request.GET)), content_type="text/plain", status=400) + return HttpResponse("ERROR: Asset ID not found in URL\n{}".format(json.dumps(request.GET)), content_type="text/plain", status=400) # Check for error in URL - error = request.GET.get('error') - error_description = request.GET.get('error_description') + error = request.GET.get("error") + error_description = request.GET.get("error_description") # If there is an error in response if error: - message = 'Error: {0}'.format(error) + message = "Error: {0}".format(error) if error_description: - message = '{0} Details: {1}'.format(message, error_description) - return HttpResponse('Server returned {0}'.format(message), content_type="text/plain", status=400) + message = "{0} Details: {1}".format(message, error_description) + return HttpResponse("Server returned {0}".format(message), content_type="text/plain", status=400) - code = request.GET.get('code') - admin_consent = request.GET.get('admin_consent') + code = request.GET.get("code") + admin_consent = request.GET.get("admin_consent") # If none of the code or admin_consent is available if not (code or admin_consent): - return HttpResponse('Error while authenticating\n{0}'.format(json.dumps(request.GET)), content_type="text/plain", status=400) + return HttpResponse("Error while authenticating\n{0}".format(json.dumps(request.GET)), content_type="text/plain", status=400) state = _load_app_state(asset_id) # If value of admin_consent is available if admin_consent: - if admin_consent == 'True': + if admin_consent == "True": admin_consent = True else: admin_consent = False - state['admin_consent'] = admin_consent + state["admin_consent"] = admin_consent _save_app_state(state, asset_id, None) # If admin_consent is True if admin_consent: - return HttpResponse('Admin Consent received. Please close this window.', content_type="text/plain") - return HttpResponse('Admin Consent declined. Please close this window and try again later.', content_type="text/plain", status=400) + return HttpResponse("Admin Consent received. Please close this window.", content_type="text/plain") + return HttpResponse("Admin Consent declined. Please close this window and try again later.", content_type="text/plain", status=400) # If value of admin_consent is not available, value of code is available - state['code'] = code + state["code"] = code try: - state['code'] = MicrosoftTeamConnector().encrypt_state(code, "code") + state["code"] = MicrosoftTeamConnector().encrypt_state(code, "code") state[MSTEAMS_STATE_IS_ENCRYPTED] = True except Exception as e: return HttpResponse("{}: {}".format(MSTEAMS_ENCRYPTION_ERROR, str(e)), content_type="text/plain", status=400) _save_app_state(state, asset_id, None) - return HttpResponse('Code received. Please close this window, the action will continue to get new token.', content_type="text/plain") + return HttpResponse("Code received. Please close this window, the action will continue to get new token.", content_type="text/plain") def _handle_rest_request(request, path_parts): - """ Handle requests for authorization. + """Handle requests for authorization. :param request: Data given to REST endpoint :param path_parts: parts of the URL passed @@ -236,52 +236,52 @@ def _handle_rest_request(request, path_parts): """ if len(path_parts) < 2: - return HttpResponse('error: True, message: Invalid REST endpoint request', content_type="text/plain", status=404) + return HttpResponse("error: True, message: Invalid REST endpoint request", content_type="text/plain", status=404) call_type = path_parts[1] # To handle admin_consent request in get_admin_consent action - if call_type == 'admin_consent': - return _handle_login_redirect(request, 'admin_consent_url') + if call_type == "admin_consent": + return _handle_login_redirect(request, "admin_consent_url") # To handle authorize request in test connectivity action - if call_type == 'start_oauth': - return _handle_login_redirect(request, 'authorization_url') + if call_type == "start_oauth": + return _handle_login_redirect(request, "authorization_url") # To handle response from microsoft login page - if call_type == 'result': + if call_type == "result": return_val = _handle_login_response(request) - asset_id = request.GET.get('state') + asset_id = request.GET.get("state") if asset_id and asset_id.isalnum(): app_dir = os.path.dirname(os.path.abspath(__file__)) - auth_status_file_path = '{0}/{1}_{2}'.format(app_dir, asset_id, MSTEAMS_TC_FILE) + auth_status_file_path = "{0}/{1}_{2}".format(app_dir, asset_id, MSTEAMS_TC_FILE) real_auth_status_file_path = os.path.abspath(auth_status_file_path) if not os.path.dirname(real_auth_status_file_path) == app_dir: return HttpResponse("Error: Invalid asset_id", content_type="text/plain", status=400) - open(auth_status_file_path, 'w').close() + open(auth_status_file_path, "w").close() try: - uid = pwd.getpwnam('apache').pw_uid - gid = grp.getgrnam('phantom').gr_gid + uid = pwd.getpwnam("apache").pw_uid + gid = grp.getgrnam("phantom").gr_gid os.chown(auth_status_file_path, uid, gid) - os.chmod(auth_status_file_path, '0664') + os.chmod(auth_status_file_path, "0664") except Exception: pass return return_val - return HttpResponse('error: Invalid endpoint', content_type="text/plain", status=404) + return HttpResponse("error: Invalid endpoint", content_type="text/plain", status=404) def _get_dir_name_from_app_name(app_name): - """ Get name of the directory for the app. + """Get name of the directory for the app. :param app_name: Name of the application for which directory name is required :return: app_name: Name of the directory for the application """ - app_name = ''.join([x for x in app_name if x.isalnum()]) + app_name = "".join([x for x in app_name if x.isalnum()]) app_name = app_name.lower() if not app_name: - app_name = 'app_for_phantom' + app_name = "app_for_phantom" return app_name @@ -308,23 +308,23 @@ def __init__(self): self._scope = None def encrypt_state(self, encrypt_var, token_name): - """ Handle encryption of token. + """Handle encryption of token. :param encrypt_var: Variable needs to be encrypted :return: encrypted variable """ - self.debug_print(MSTEAMS_ENCRYPT_TOKEN.format(token_name)) # nosemgrep + self.debug_print(MSTEAMS_ENCRYPT_TOKEN.format(token_name)) # nosemgrep return encryption_helper.encrypt(encrypt_var, self.asset_id) def decrypt_state(self, decrypt_var, token_name): - """ Handle decryption of token. + """Handle decryption of token. :param decrypt_var: Variable needs to be decrypted :return: decrypted variable """ - self.debug_print(MSTEAMS_DECRYPT_TOKEN.format(token_name)) # nosemgrep + self.debug_print(MSTEAMS_DECRYPT_TOKEN.format(token_name)) # nosemgrep return encryption_helper.decrypt(decrypt_var, self.asset_id) def _process_empty_response(self, response, action_result): - """ This function is used to process empty response. + """This function is used to process empty response. :param response: response data :param action_result: object of Action Result @@ -335,11 +335,15 @@ def _process_empty_response(self, response, action_result): if response.status_code in [200, 204]: return RetVal(phantom.APP_SUCCESS, {}) - return RetVal(action_result.set_status(phantom.APP_ERROR, "Status code: {}. Empty response and no information in the header".format( - response.status_code)), None) + return RetVal( + action_result.set_status( + phantom.APP_ERROR, "Status code: {}. Empty response and no information in the header".format(response.status_code) + ), + None, + ) def _process_html_response(self, response, action_result) -> RetVal[bool, Optional[Any]]: - """ This function is used to process html response. + """This function is used to process html response. :param response: response data :param action_result: object of Action Result @@ -355,21 +359,20 @@ def _process_html_response(self, response, action_result) -> RetVal[bool, Option for element in soup(["script", "style", "footer", "nav"]): element.extract() error_text = soup.text - split_lines = error_text.split('\n') + split_lines = error_text.split("\n") split_lines = [x.strip() for x in split_lines if x.strip()] - error_text = '\n'.join(split_lines) + error_text = "\n".join(split_lines) except Exception: error_text = "Cannot parse error details" - message = "Status Code: {0}. Data from server:\n{1}\n".format(status_code, - error_text) + message = "Status Code: {0}. Data from server:\n{1}\n".format(status_code, error_text) - message = message.replace('{', '{{').replace('}', '}}') + message = message.replace("{", "{{").replace("}", "}}") return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) def _process_json_response(self, response, action_result) -> RetVal[bool, Optional[Any]]: - """ This function is used to process json response. + """This function is used to process json response. :param response: response data :param action_result: object of Action Result @@ -387,18 +390,18 @@ def _process_json_response(self, response, action_result) -> RetVal[bool, Option if 200 <= response.status_code < 399: return RetVal(phantom.APP_SUCCESS, resp_json) - error_message = response.text.replace('{', '{{').replace('}', '}}') + error_message = response.text.replace("{", "{{").replace("}", "}}") message = "Error from server. Status Code: {0} Data from server: {1}".format(response.status_code, error_message) # Show only error message if available - if isinstance(resp_json.get('error', {}), dict) and resp_json.get('error', {}).get('message'): - error_message = resp_json['error']['message'] + if isinstance(resp_json.get("error", {}), dict) and resp_json.get("error", {}).get("message"): + error_message = resp_json["error"]["message"] message = "Error from server. Status Code: {0} Data from server: {1}".format(response.status_code, error_message) return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) def _process_response(self, response, action_result) -> RetVal[bool, Optional[Any]]: - """ This function is used to process html response. + """This function is used to process html response. :param response: response data :param action_result: object of Action Result @@ -406,25 +409,25 @@ def _process_response(self, response, action_result) -> RetVal[bool, Optional[An """ # store the r_text in debug data, it will get dumped in the logs if the action fails - if hasattr(action_result, 'add_debug_data'): - action_result.add_debug_data({'r_status_code': response.status_code}) - action_result.add_debug_data({'r_text': response.text}) - action_result.add_debug_data({'r_headers': response.headers}) + if hasattr(action_result, "add_debug_data"): + action_result.add_debug_data({"r_status_code": response.status_code}) + action_result.add_debug_data({"r_text": response.text}) + action_result.add_debug_data({"r_headers": response.headers}) # Process each 'Content-Type' of response separately # Process a json response - if 'json' in response.headers.get('Content-Type', ''): + if "json" in response.headers.get("Content-Type", ""): return self._process_json_response(response, action_result) - if 'text/javascript' in response.headers.get('Content-Type', ''): + if "text/javascript" in response.headers.get("Content-Type", ""): return self._process_json_response(response, action_result) # Process an HTML response, Do this no matter what the API talks. # There is a high chance of a PROXY in between phantom and the rest of # world, in case of errors, PROXY's return HTML, this function parses # the error and adds it to the action_result. - if 'html' in response.headers.get('Content-Type', ''): + if "html" in response.headers.get("Content-Type", ""): return self._process_html_response(response, action_result) # it's not content-type that is to be parsed, handle an empty response @@ -432,22 +435,13 @@ def _process_response(self, response, action_result) -> RetVal[bool, Optional[An return self._process_empty_response(response, action_result) # everything else is actually an error at this point - error_message = response.text.replace('{', '{{').replace('}', '}}') - message = "Can't process response from server. Status Code: {0} Data from server: {1}".format( - response.status_code, error_message) + error_message = response.text.replace("{", "{{").replace("}", "}}") + message = "Can't process response from server. Status Code: {0} Data from server: {1}".format(response.status_code, error_message) return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - def _update_request( - self, - action_result, - endpoint, - headers=None, - params=None, - data=None, - method='get' - ) -> tuple[bool, Optional[Any]]: - """ This function is used to update the headers with access_token before making REST call. + def _update_request(self, action_result, endpoint, headers=None, params=None, data=None, method="get") -> tuple[bool, Optional[Any]]: + """This function is used to update the headers with access_token before making REST call. :param endpoint: REST endpoint that needs to appended to the service address :param action_result: object of ActionResult class @@ -462,9 +456,9 @@ def _update_request( # In pagination, URL of next page contains complete URL # So no need to modify them if endpoint.startswith(MSTEAMS_MSGRAPH_TEAMS_ENDPOINT): - endpoint = '{0}{1}'.format(MSTEAMS_MSGRAPH_BETA_API_BASE_URL, endpoint) + endpoint = "{0}{1}".format(MSTEAMS_MSGRAPH_BETA_API_BASE_URL, endpoint) elif not endpoint.startswith(MSTEAMS_MSGRAPH_API_BASE_URL): - endpoint = '{0}{1}'.format(MSTEAMS_MSGRAPH_API_BASE_URL, endpoint) + endpoint = "{0}{1}".format(MSTEAMS_MSGRAPH_API_BASE_URL, endpoint) if headers is None: headers = {} @@ -472,11 +466,11 @@ def _update_request( self._client_id = urllib.quote(self._client_id) self._tenant = urllib.quote(self._tenant) token_data = { - 'client_id': self._client_id, - 'scope': self._scope, - 'client_secret': self._client_secret, - 'grant_type': MSTEAMS_REFRESH_TOKEN_STRING, - 'refresh_token': self._refresh_token + "client_id": self._client_id, + "scope": self._scope, + "client_secret": self._client_secret, + "grant_type": MSTEAMS_REFRESH_TOKEN_STRING, + "refresh_token": self._refresh_token, } if not self._access_token: @@ -490,17 +484,12 @@ def _update_request( if phantom.is_fail(status): return action_result.get_status(), None - headers.update({'Authorization': 'Bearer {0}'.format(self._access_token), - 'Accept': 'application/json', - 'Content-Type': 'application/json'}) + headers.update( + {"Authorization": "Bearer {0}".format(self._access_token), "Accept": "application/json", "Content-Type": "application/json"} + ) status, resp_json = self._make_rest_call( - action_result=action_result, - endpoint=endpoint, - headers=headers, - params=params, - data=data, - method=method + action_result=action_result, endpoint=endpoint, headers=headers, params=params, data=data, method=method ) action_result_message = action_result.get_message().lower() @@ -514,15 +503,10 @@ def _update_request( if phantom.is_fail(status): return action_result.get_status(), None - headers['Authorization'] = f"Bearer {self._access_token}" + headers["Authorization"] = f"Bearer {self._access_token}" status, resp_json = self._make_rest_call( - action_result=action_result, - endpoint=endpoint, - headers=headers, - params=params, - data=data, - method=method + action_result=action_result, endpoint=endpoint, headers=headers, params=params, data=data, method=method ) if phantom.is_fail(status): @@ -536,16 +520,9 @@ def _is_token_expired(self, action_result_message: str) -> bool: return MSTEAMS_TOKEN_EXPIRED_MARKER in action_result_message def _make_rest_call( - self, - endpoint, - action_result, - headers=None, - params=None, - data=None, - method="get", - verify=True + self, endpoint, action_result, headers=None, params=None, data=None, method="get", verify=True ) -> RetVal[bool, Optional[Any]]: - """ Function that makes the REST call to the app. + """Function that makes the REST call to the app. :param endpoint: REST endpoint that needs to appended to the service address :param action_result: object of ActionResult class @@ -573,7 +550,7 @@ def _make_rest_call( return self._process_response(r, action_result) def _get_asset_name(self, action_result): - """ Get name of the asset using Phantom URL. + """Get name of the asset using Phantom URL. :param action_result: object of ActionResult class :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message), asset name @@ -581,31 +558,30 @@ def _get_asset_name(self, action_result): asset_id = self.get_asset_id() rest_endpoint = MSTEAMS_PHANTOM_ASSET_INFO_URL.format(asset_id=asset_id) - url = '{}{}'.format(self.get_phantom_base_url() + 'rest', rest_endpoint) + url = "{}{}".format(self.get_phantom_base_url() + "rest", rest_endpoint) status, resp_json = self._make_rest_call(action_result=action_result, endpoint=url, verify=False) if phantom.is_fail(status): return status, None - asset_name = resp_json.get('name') + asset_name = resp_json.get("name") if not asset_name: - return action_result.set_status(phantom.APP_ERROR, 'Asset Name for id: {0} not found.'.format(asset_id), - None) + return action_result.set_status(phantom.APP_ERROR, "Asset Name for id: {0} not found.".format(asset_id), None) return phantom.APP_SUCCESS, asset_name def _get_phantom_base_url_ms(self, action_result): - """ Get base url of phantom. + """Get base url of phantom. :param action_result: object of ActionResult class :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message), base url of phantom """ - url = '{}{}'.format(self.get_phantom_base_url() + 'rest', MSTEAMS_PHANTOM_SYS_INFO_URL) + url = "{}{}".format(self.get_phantom_base_url() + "rest", MSTEAMS_PHANTOM_SYS_INFO_URL) status, resp_json = self._make_rest_call(action_result=action_result, endpoint=url, verify=False) if phantom.is_fail(status): return status, None - phantom_base_url = resp_json.get('base_url') + phantom_base_url = resp_json.get("base_url") if not phantom_base_url: return action_result.set_status(phantom.APP_ERROR, MSTEAMS_BASE_URL_NOT_FOUND_MSG), None @@ -614,7 +590,7 @@ def _get_phantom_base_url_ms(self, action_result): return phantom.APP_SUCCESS, phantom_base_url def _get_app_rest_url(self, action_result): - """ Get URL for making rest calls. + """Get URL for making rest calls. :param action_result: object of ActionResult class :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message), @@ -629,31 +605,25 @@ def _get_app_rest_url(self, action_result): if phantom.is_fail(ret_val): return action_result.get_status(), None - self.save_progress('Using Phantom base URL as: {0}'.format(phantom_base_url)) + self.save_progress("Using Phantom base URL as: {0}".format(phantom_base_url)) app_json = self.get_app_json() - app_name = app_json['name'] + app_name = app_json["name"] app_dir_name = _get_dir_name_from_app_name(app_name) - url_to_app_rest = '{0}/rest/handler/{1}_{2}/{3}'.format(phantom_base_url, app_dir_name, app_json['appid'], - asset_name) + url_to_app_rest = "{0}/rest/handler/{1}_{2}/{3}".format(phantom_base_url, app_dir_name, app_json["appid"], asset_name) return phantom.APP_SUCCESS, url_to_app_rest def _generate_new_access_token(self, action_result, data) -> bool: - """ This function is used to generate new access token using the code obtained on authorization. + """This function is used to generate new access token using the code obtained on authorization. :param action_result: object of ActionResult class :param data: Data to send in REST call :return: status phantom.APP_ERROR/phantom.APP_SUCCESS """ - req_url = '{}{}'.format(MSTEAMS_LOGIN_BASE_URL, MSTEAMS_SERVER_TOKEN_URL.format(tenant_id=self._tenant)) + req_url = "{}{}".format(MSTEAMS_LOGIN_BASE_URL, MSTEAMS_SERVER_TOKEN_URL.format(tenant_id=self._tenant)) - status, resp_json = self._make_rest_call( - action_result=action_result, - endpoint=req_url, - data=urllib.urlencode(data), - method="post" - ) + status, resp_json = self._make_rest_call(action_result=action_result, endpoint=req_url, data=urllib.urlencode(data), method="post") if phantom.is_fail(status): return action_result.get_status() @@ -689,9 +659,11 @@ def _generate_new_access_token(self, action_result, data) -> bool: # after successful generation of new token are same or not. try: - if self._access_token != self.decrypt_state(self._state.get(MSTEAMS_TOKEN_STRING, {}).get - (MSTEAMS_ACCESS_TOKEN_STRING), "access") or self._refresh_token != self.decrypt_state(self._state.get - (MSTEAMS_TOKEN_STRING, {}).get(MSTEAMS_REFRESH_TOKEN_STRING), "refresh"): + if self._access_token != self.decrypt_state( + self._state.get(MSTEAMS_TOKEN_STRING, {}).get(MSTEAMS_ACCESS_TOKEN_STRING), "access" + ) or self._refresh_token != self.decrypt_state( + self._state.get(MSTEAMS_TOKEN_STRING, {}).get(MSTEAMS_REFRESH_TOKEN_STRING), "refresh" + ): message = "Error occurred while saving the newly generated access or " message += "refresh token (in place of the expired token) in the state file." message += " Please check the owner, owner group, and the permissions of the state file. The Phantom " @@ -705,7 +677,7 @@ def _generate_new_access_token(self, action_result, data) -> bool: return action_result.set_status(phantom.APP_SUCCESS, status_message=MSTEAMS_TOKEN_GENERATED_MSG) def _handle_test_connectivity(self, param): - """ Testing of given credentials and obtaining authorization/admin consent for all other actions. + """Testing of given credentials and obtaining authorization/admin consent for all other actions. :param param: (not used in this method) :return: status success/failure @@ -721,8 +693,8 @@ def _handle_test_connectivity(self, param): return action_result.set_status(phantom.APP_ERROR, status_message=MSTEAMS_TEST_CONNECTIVITY_FAILED_MSG) # Append /result to create redirect_uri - redirect_uri = '{0}/result'.format(app_rest_url) - app_state['redirect_uri'] = redirect_uri + redirect_uri = "{0}/result".format(app_rest_url) + app_state["redirect_uri"] = redirect_uri self.save_progress(MSTEAMS_OAUTH_URL_MSG) self.save_progress(redirect_uri) @@ -730,20 +702,24 @@ def _handle_test_connectivity(self, param): # Authorization URL used to make request for getting code which is used to generate access token self._client_id = urllib.quote(self._client_id) self._tenant = urllib.quote(self._tenant) - authorization_url = MSTEAMS_AUTHORIZE_URL.format(tenant_id=self._tenant, client_id=self._client_id, - redirect_uri=redirect_uri, state=self.get_asset_id(), - response_type='code', - scope=self._scope) - authorization_url = '{}{}'.format(MSTEAMS_LOGIN_BASE_URL, authorization_url) + authorization_url = MSTEAMS_AUTHORIZE_URL.format( + tenant_id=self._tenant, + client_id=self._client_id, + redirect_uri=redirect_uri, + state=self.get_asset_id(), + response_type="code", + scope=self._scope, + ) + authorization_url = "{}{}".format(MSTEAMS_LOGIN_BASE_URL, authorization_url) - app_state['authorization_url'] = authorization_url + app_state["authorization_url"] = authorization_url # URL which would be shown to the user - url_for_authorize_request = '{0}/start_oauth?asset_id={1}&'.format(app_rest_url, self.get_asset_id()) + url_for_authorize_request = "{0}/start_oauth?asset_id={1}&".format(app_rest_url, self.get_asset_id()) _save_app_state(app_state, self.get_asset_id(), self) self.save_progress(MSTEAMS_AUTHORIZE_USER_MSG) - self.save_progress(url_for_authorize_request) # nosemgrep + self.save_progress(url_for_authorize_request) # nosemgrep self.save_progress(MSTEAMS_AUTHORIZE_TROUBLESHOOT_MSG) self.save_progress(MSTEAMS_AUTHORIZE_WAIT_MSG) @@ -757,17 +733,17 @@ def _handle_test_connectivity(self, param): return action_result.get_status() # Empty message to override last message of waiting - self.send_progress('') + self.send_progress("") self.save_progress(MSTEAMS_CODE_RECEIVED_MSG) self._state = _load_app_state(self.get_asset_id(), self) # if code is not available in the state file - if not self._state or not self._state.get('code'): + if not self._state or not self._state.get("code"): return action_result.set_status(phantom.APP_ERROR, status_message=MSTEAMS_TEST_CONNECTIVITY_FAILED_MSG) if self._state.get(MSTEAMS_STATE_IS_ENCRYPTED): try: - current_code = self.decrypt_state(self._state['code'], "code") + current_code = self.decrypt_state(self._state["code"], "code") except Exception as e: self.debug_print("{}: {}".format(MSTEAMS_DECRYPTION_ERROR, _get_error_message_from_exception(e, self))) return action_result.set_status(phantom.APP_ERROR, MSTEAMS_DECRYPTION_ERROR) @@ -776,12 +752,12 @@ def _handle_test_connectivity(self, param): self.save_progress(MSTEAMS_GENERATING_ACCESS_TOKEN_MSG) data = { - 'client_id': self._client_id, - 'scope': self._scope, - 'client_secret': self._client_secret, - 'grant_type': 'authorization_code', - 'redirect_uri': redirect_uri, - 'code': current_code + "client_id": self._client_id, + "scope": self._scope, + "client_secret": self._client_secret, + "grant_type": "authorization_code", + "redirect_uri": redirect_uri, + "code": current_code, } # for first time access, new access token is generated ret_val = self._generate_new_access_token(action_result=action_result, data=data) @@ -792,7 +768,7 @@ def _handle_test_connectivity(self, param): self.save_progress(MSTEAMS_CURRENT_USER_INFO_MSG) - url = '{}{}'.format(MSTEAMS_MSGRAPH_API_BASE_URL, MSTEAMS_MSGRAPH_SELF_ENDPOINT) + url = "{}{}".format(MSTEAMS_MSGRAPH_API_BASE_URL, MSTEAMS_MSGRAPH_SELF_ENDPOINT) status, response = self._update_request(action_result=action_result, endpoint=url) if phantom.is_fail(status): @@ -804,7 +780,7 @@ def _handle_test_connectivity(self, param): return action_result.set_status(phantom.APP_SUCCESS) def _wait(self, action_result): - """ This function is used to hold the action till user login. + """This function is used to hold the action till user login. :param action_result: Object of ActionResult class :return: status (success/failed) @@ -812,7 +788,7 @@ def _wait(self, action_result): app_dir = os.path.dirname(os.path.abspath(__file__)) # file to check whether the request has been granted or not - auth_status_file_path = '{0}/{1}_{2}'.format(app_dir, self.get_asset_id(), MSTEAMS_TC_FILE) + auth_status_file_path = "{0}/{1}_{2}".format(app_dir, self.get_asset_id(), MSTEAMS_TC_FILE) time_out = False # wait-time while request is being granted @@ -824,12 +800,12 @@ def _wait(self, action_result): time.sleep(MSTEAMS_TC_STATUS_SLEEP) if not time_out: - return action_result.set_status(phantom.APP_ERROR, status_message='Timeout. Please try again later.') - self.send_progress('Authenticated') + return action_result.set_status(phantom.APP_ERROR, status_message="Timeout. Please try again later.") + self.send_progress("Authenticated") return phantom.APP_SUCCESS def _handle_get_admin_consent(self, param): - """ This function is used to get the consent from admin. + """This function is used to get the consent from admin. :param param: Dictionary of input parameters :return: status success/failure @@ -840,27 +816,29 @@ def _handle_get_admin_consent(self, param): ret_val, app_rest_url = self._get_app_rest_url(action_result) if phantom.is_fail(ret_val): - return action_result.set_status(phantom.APP_ERROR, - status_message="Unable to get the URL to the app's REST Endpoint. " - "Error: {0}".format(action_result.get_message())) - redirect_uri = '{0}/result'.format(app_rest_url) + return action_result.set_status( + phantom.APP_ERROR, + status_message="Unable to get the URL to the app's REST Endpoint. " "Error: {0}".format(action_result.get_message()), + ) + redirect_uri = "{0}/result".format(app_rest_url) # Store admin_consent_url to state file so that we can access it from _handle_rest_request self._client_id = urllib.quote(self._client_id) self._tenant = urllib.quote(self._tenant) - admin_consent_url = MSTEAMS_ADMIN_CONSENT_URL.format(tenant_id=self._tenant, client_id=self._client_id, - redirect_uri=redirect_uri, state=self.get_asset_id()) - admin_consent_url = '{}{}'.format(MSTEAMS_LOGIN_BASE_URL, admin_consent_url) - self._state['admin_consent_url'] = admin_consent_url + admin_consent_url = MSTEAMS_ADMIN_CONSENT_URL.format( + tenant_id=self._tenant, client_id=self._client_id, redirect_uri=redirect_uri, state=self.get_asset_id() + ) + admin_consent_url = "{}{}".format(MSTEAMS_LOGIN_BASE_URL, admin_consent_url) + self._state["admin_consent_url"] = admin_consent_url - url_to_show = '{0}/admin_consent?asset_id={1}&'.format(app_rest_url, self.get_asset_id()) + url_to_show = "{0}/admin_consent?asset_id={1}&".format(app_rest_url, self.get_asset_id()) _save_app_state(self._state, self.get_asset_id(), self) - self.save_progress('Waiting to receive the admin consent') - self.debug_print('Waiting to receive the admin consent') + self.save_progress("Waiting to receive the admin consent") + self.debug_print("Waiting to receive the admin consent") - self.save_progress('{0}{1}'.format(MSTEAMS_ADMIN_CONSENT_MSG, url_to_show)) - self.debug_print('{0}{1}'.format(MSTEAMS_ADMIN_CONSENT_MSG, url_to_show)) + self.save_progress("{0}{1}".format(MSTEAMS_ADMIN_CONSENT_MSG, url_to_show)) + self.debug_print("{0}{1}".format(MSTEAMS_ADMIN_CONSENT_MSG, url_to_show)) time.sleep(MSTEAMS_AUTHORIZE_WAIT_TIME) @@ -871,13 +849,13 @@ def _handle_get_admin_consent(self, param): self._state = _load_app_state(self.get_asset_id(), self) - if not self._state or not self._state.get('admin_consent'): + if not self._state or not self._state.get("admin_consent"): return action_result.set_status(phantom.APP_ERROR, status_message=MSTEAMS_ADMIN_CONSENT_FAILED_MSG) return action_result.set_status(phantom.APP_SUCCESS, status_message=MSTEAMS_ADMIN_CONSENT_PASSED_MSG) def _handle_list_users(self, param): - """ This function is used to list all the users. + """This function is used to list all the users. :param param: Dictionary of input parameters :return: status success/failure @@ -897,7 +875,7 @@ def _handle_list_users(self, param): if phantom.is_fail(status): return action_result.get_status() - for user in response.get('value', []): + for user in response.get("value", []): action_result.add_data(user) if not response.get(MSTEAMS_NEXT_LINK_STRING): @@ -906,12 +884,12 @@ def _handle_list_users(self, param): endpoint = response[MSTEAMS_NEXT_LINK_STRING] summary = action_result.update_summary({}) - summary['total_users'] = action_result.get_data_size() + summary["total_users"] = action_result.get_data_size() return action_result.set_status(phantom.APP_SUCCESS) def _verify_parameters(self, group_id, channel_id, action_result) -> bool: - """ This function is used to verify that the provided group_id is valid and channel_id belongs + """This function is used to verify that the provided group_id is valid and channel_id belongs to that group_id. :param group_id: ID of group @@ -930,8 +908,8 @@ def _verify_parameters(self, group_id, channel_id, action_result) -> bool: if phantom.is_fail(status): return action_result.get_status() - for channel in response.get('value', []): - channel_list.append(channel['id']) + for channel in response.get("value", []): + channel_list.append(channel["id"]) if not response.get(MSTEAMS_NEXT_LINK_STRING): break @@ -939,13 +917,14 @@ def _verify_parameters(self, group_id, channel_id, action_result) -> bool: endpoint = response[MSTEAMS_NEXT_LINK_STRING] if channel_id not in channel_list: - return action_result.set_status(phantom.APP_ERROR, status_message=MSTEAMS_INVALID_CHANNEL_MSG.format( - channel_id=channel_id, group_id=group_id)) + return action_result.set_status( + phantom.APP_ERROR, status_message=MSTEAMS_INVALID_CHANNEL_MSG.format(channel_id=channel_id, group_id=group_id) + ) return phantom.APP_SUCCESS def _handle_send_message(self, param): - """ This function is used to send the message in a group. + """This function is used to send the message in a group. :param param: Dictionary of input parameters :return: status success/failure @@ -962,35 +941,29 @@ def _handle_send_message(self, param): if phantom.is_fail(status): error_message = action_result.get_message() - if 'teamId' in error_message: - error_message = error_message.replace('teamId', "'group_id'") + if "teamId" in error_message: + error_message = error_message.replace("teamId", "'group_id'") return action_result.set_status(phantom.APP_ERROR, error_message) endpoint = MSTEAMS_MSGRAPH_SEND_MSG_ENDPOINT.format(group_id=group_id, channel_id=channel_id) - data = { - "body": { - "contentType": "html", - "content": message - } - } + data = {"body": {"contentType": "html", "content": message}} # make rest call - status, response = self._update_request(endpoint=endpoint, action_result=action_result, method='post', - data=json.dumps(data)) + status, response = self._update_request(endpoint=endpoint, action_result=action_result, method="post", data=json.dumps(data)) if phantom.is_fail(status): error_message = action_result.get_message() - if 'teamId' in error_message: - error_message = error_message.replace('teamId', "'group_id'") + if "teamId" in error_message: + error_message = error_message.replace("teamId", "'group_id'") return action_result.set_status(phantom.APP_ERROR, error_message) action_result.add_data(response) - return action_result.set_status(phantom.APP_SUCCESS, status_message='Message sent') + return action_result.set_status(phantom.APP_SUCCESS, status_message="Message sent") def _handle_list_channels(self, param): - """ This function is used to list all the channels of the particular group. + """This function is used to list all the channels of the particular group. :param param: Dictionary of input parameters :return: status phantom.APP_SUCCESS/phantom.APP_ERROR @@ -1010,11 +983,11 @@ def _handle_list_channels(self, param): if phantom.is_fail(status): error_message = action_result.get_message() - if 'teamId' in error_message: - error_message = error_message.replace('teamId', "'group_id'") + if "teamId" in error_message: + error_message = error_message.replace("teamId", "'group_id'") return action_result.set_status(phantom.APP_ERROR, error_message) - for channel in response.get('value', []): + for channel in response.get("value", []): action_result.add_data(channel) if not response.get(MSTEAMS_NEXT_LINK_STRING): @@ -1023,12 +996,12 @@ def _handle_list_channels(self, param): endpoint = response[MSTEAMS_NEXT_LINK_STRING] summary = action_result.update_summary({}) - summary['total_channels'] = action_result.get_data_size() + summary["total_channels"] = action_result.get_data_size() return action_result.set_status(phantom.APP_SUCCESS) def _handle_list_groups(self, param): - """ This function is used to list all the groups for Microsoft Team. + """This function is used to list all the groups for Microsoft Team. :param param: Dictionary of input parameters :return: status success/failure @@ -1046,7 +1019,7 @@ def _handle_list_groups(self, param): if phantom.is_fail(status): return action_result.get_status() - for group in response.get('value', []): + for group in response.get("value", []): action_result.add_data(group) if not response.get(MSTEAMS_NEXT_LINK_STRING): @@ -1055,12 +1028,12 @@ def _handle_list_groups(self, param): endpoint = response[MSTEAMS_NEXT_LINK_STRING] summary = action_result.update_summary({}) - summary['total_groups'] = action_result.get_data_size() + summary["total_groups"] = action_result.get_data_size() return action_result.set_status(phantom.APP_SUCCESS) def _handle_list_teams(self, param): - """ This function is used to list all the teams for Microsoft Team. + """This function is used to list all the teams for Microsoft Team. :param param: Dictionary of input parameters :return: status success/failure @@ -1078,7 +1051,7 @@ def _handle_list_teams(self, param): if phantom.is_fail(status): return action_result.get_status() - for team in response.get('value', []): + for team in response.get("value", []): action_result.add_data(team) if not response.get(MSTEAMS_NEXT_LINK_STRING): @@ -1087,12 +1060,12 @@ def _handle_list_teams(self, param): endpoint = response[MSTEAMS_NEXT_LINK_STRING] summary = action_result.update_summary({}) - summary['total_teams'] = action_result.get_data_size() + summary["total_teams"] = action_result.get_data_size() return action_result.set_status(phantom.APP_SUCCESS) def _handle_create_meeting(self, param): - """ This function is used to create meeting for Microsoft Teams. + """This function is used to create meeting for Microsoft Teams. :param param: Dictionary of input parameters :return: status success/failure @@ -1105,9 +1078,7 @@ def _handle_create_meeting(self, param): subject = param.get(MSTEAMS_JSON_SUBJECT) data = {} if subject: - data.update({ - "subject": subject - }) + data.update({"subject": subject}) if not use_calendar: endpoint = MSTEAMS_MSGRAPH_ONLINE_MEETING_ENDPOINT else: @@ -1121,48 +1092,29 @@ def _handle_create_meeting(self, param): attendees = [value.strip() for value in attendees.split(",")] attendees = list(filter(None, attendees)) for attendee in attendees: - attendee_dict = {"emailAddress": { - "address": attendee - }} + attendee_dict = {"emailAddress": {"address": attendee}} attendees_list.append(attendee_dict) - data.update({ "isOnlineMeeting": True }) + data.update({"isOnlineMeeting": True}) if description: - data.update({ - "body": { - "content": description - } - }) + data.update({"body": {"content": description}}) if start_time: - data.update({ - "start": { - "dateTime": start_time, - "timeZone": self._timezone - } - }) + data.update({"start": {"dateTime": start_time, "timeZone": self._timezone}}) if end_time: - data.update({ - "end": { - "dateTime": end_time, - "timeZone": self._timezone - } - }) + data.update({"end": {"dateTime": end_time, "timeZone": self._timezone}}) if attendees_list: - data.update({ - "attendees": attendees_list - }) + data.update({"attendees": attendees_list}) # make rest call - status, response = self._update_request(endpoint=endpoint, action_result=action_result, method='post', - data=json.dumps(data)) + status, response = self._update_request(endpoint=endpoint, action_result=action_result, method="post", data=json.dumps(data)) if phantom.is_fail(status): return action_result.get_status() action_result.add_data(response) - return action_result.set_status(phantom.APP_SUCCESS, status_message='Meeting Created Successfully') + return action_result.set_status(phantom.APP_SUCCESS, status_message="Meeting Created Successfully") def _handle_list_chats(self, param): - """ This function is used to list all chats for the current user with optional filters. + """This function is used to list all chats for the current user with optional filters. :param param: Dictionary of input parameters :return: status success/failure @@ -1186,16 +1138,16 @@ def _handle_list_chats(self, param): if phantom.is_fail(status): return action_result.get_status() - for chat in response.get('value', []): + for chat in response.get("value", []): # Filters - if chat_type_filter and chat_type_filter != chat.get('chatType', ''): + if chat_type_filter and chat_type_filter != chat.get("chatType", ""): continue if user_filter: user_match = False - for member in chat.get('members', []): - user_id = member.get('userId', '') - email = member.get('email', '') + for member in chat.get("members", []): + user_id = member.get("userId", "") + email = member.get("email", "") if user_filter in user_id or user_filter in email: user_match = True break @@ -1210,30 +1162,24 @@ def _handle_list_chats(self, param): endpoint = response[MSTEAMS_NEXT_LINK_STRING] summary = action_result.update_summary({}) - summary['total_chats'] = action_result.get_data_size() + summary["total_chats"] = action_result.get_data_size() return action_result.set_status(phantom.APP_SUCCESS) def _send_chat_message(self, action_result, chat_id, message): - """ This function is used to send a message to a chat. + """This function is used to send a message to a chat. :param action_result: Object of ActionResult class :param chat_id: ID of the chat :param message: Message to be sent :return: status success/failure, response """ - endpoint = f'/chats/{chat_id}/messages' + endpoint = f"/chats/{chat_id}/messages" - data = { - "body": { - "contentType": "html", - "content": message - } - } + data = {"body": {"contentType": "html", "content": message}} # make rest call - status, response = self._update_request(endpoint=endpoint, action_result=action_result, method='post', - data=json.dumps(data)) + status, response = self._update_request(endpoint=endpoint, action_result=action_result, method="post", data=json.dumps(data)) if phantom.is_fail(status): return action_result.get_status(), None @@ -1241,7 +1187,7 @@ def _send_chat_message(self, action_result, chat_id, message): return phantom.APP_SUCCESS, response def _handle_send_chat_message(self, param): - """ This function is used to send a message to a chat. + """This function is used to send a message to a chat. :param param: Dictionary of input parameters :return: status success/failure @@ -1260,10 +1206,10 @@ def _handle_send_chat_message(self, param): action_result.add_data(response) - return action_result.set_status(phantom.APP_SUCCESS, status_message='Message sent to chat successfully') + return action_result.set_status(phantom.APP_SUCCESS, status_message="Message sent to chat successfully") def _handle_send_direct_message(self, param): - """ This function is used to send a direct message to a user. + """This function is used to send a direct message to a user. :param param: Dictionary of input parameters :return: status success/failure @@ -1281,7 +1227,7 @@ def _handle_send_direct_message(self, param): if phantom.is_fail(status): return action_result.set_status(phantom.APP_ERROR, "Failed to retrieve current user information") - current_user_id = me_response.get('id') + current_user_id = me_response.get("id") if not current_user_id: return action_result.set_status(phantom.APP_ERROR, "Failed to retrieve current user ID") @@ -1292,37 +1238,39 @@ def _handle_send_direct_message(self, param): return action_result.get_status() chat_id = None - for chat in response.get('value', []): - if chat.get('chatType') == 'oneOnOne': - members = chat.get('members', []) - if len(members) == 2 and any(member.get('userId') == user_id for member in members): - chat_id = chat.get('id') + for chat in response.get("value", []): + if chat.get("chatType") == "oneOnOne": + members = chat.get("members", []) + if len(members) == 2 and any(member.get("userId") == user_id for member in members): + chat_id = chat.get("id") break if not chat_id: # Create new chat if none exists - create_chat_endpoint = '/chats' + create_chat_endpoint = "/chats" create_chat_data = { "chatType": "oneOnOne", "members": [ { "@odata.type": "#microsoft.graph.aadUserConversationMember", "roles": ["owner"], - "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{current_user_id}')" + "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{current_user_id}')", }, { "@odata.type": "#microsoft.graph.aadUserConversationMember", "roles": ["owner"], - "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')" - } - ] + "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')", + }, + ], } - status, response = self._update_request(endpoint=create_chat_endpoint, action_result=action_result, method='post', data=json.dumps(create_chat_data)) + status, response = self._update_request( + endpoint=create_chat_endpoint, action_result=action_result, method="post", data=json.dumps(create_chat_data) + ) if phantom.is_fail(status): return action_result.get_status() - chat_id = response.get('id') + chat_id = response.get("id") # Send chat message now status, response = self._send_chat_message(action_result, chat_id, message) @@ -1332,10 +1280,10 @@ def _handle_send_direct_message(self, param): action_result.add_data(response) - return action_result.set_status(phantom.APP_SUCCESS, status_message='Message sent to user successfully') + return action_result.set_status(phantom.APP_SUCCESS, status_message="Message sent to user successfully") def handle_action(self, param): - """ This function gets current action identifier and calls member function of its own to handle the action. + """This function gets current action identifier and calls member function of its own to handle the action. :param param: dictionary which contains information about the actions to be executed :return: status success/failure @@ -1345,17 +1293,17 @@ def handle_action(self, param): # Dictionary mapping each action with its corresponding actions action_mapping = { - 'test_connectivity': self._handle_test_connectivity, - 'send_message': self._handle_send_message, - 'send_direct_message': self._handle_send_direct_message, - 'send_chat_message': self._handle_send_chat_message, - 'list_groups': self._handle_list_groups, - 'list_teams': self._handle_list_teams, - 'list_users': self._handle_list_users, - 'list_channels': self._handle_list_channels, - 'list_chats': self._handle_list_chats, - 'get_admin_consent': self._handle_get_admin_consent, - 'create_meeting': self._handle_create_meeting + "test_connectivity": self._handle_test_connectivity, + "send_message": self._handle_send_message, + "send_direct_message": self._handle_send_direct_message, + "send_chat_message": self._handle_send_chat_message, + "list_groups": self._handle_list_groups, + "list_teams": self._handle_list_teams, + "list_users": self._handle_list_users, + "list_channels": self._handle_list_channels, + "list_chats": self._handle_list_chats, + "get_admin_consent": self._handle_get_admin_consent, + "create_meeting": self._handle_create_meeting, } action = self.get_action_identifier() @@ -1368,7 +1316,7 @@ def handle_action(self, param): return action_execution_status def initialize(self): - """ This is an optional function that can be implemented by the AppConnector derived class. Since the + """This is an optional function that can be implemented by the AppConnector derived class. Since the configuration dictionary is already validated by the time this function is called, it's a good place to do any extra initialization of any internal modules. This function MUST return a value of either phantom.APP_SUCCESS or phantom.APP_ERROR. If this function returns phantom.APP_ERROR, then AppConnector::handle_action will not get @@ -1414,7 +1362,7 @@ def initialize(self): return phantom.APP_SUCCESS def finalize(self): - """ This function gets called once all the param dictionary elements are looped over and no more handle_action + """This function gets called once all the param dictionary elements are looped over and no more handle_action calls are left to be made. It gives the AppConnector a chance to loop through all the results that were accumulated by multiple handle_action function calls and create any summary if required. Another usage is cleanup, disconnect from remote devices, etc. @@ -1441,7 +1389,7 @@ def finalize(self): return phantom.APP_SUCCESS -if __name__ == '__main__': +if __name__ == "__main__": import argparse @@ -1451,10 +1399,10 @@ def finalize(self): argparser = argparse.ArgumentParser() - argparser.add_argument('input_test_json', help='Input Test JSON file') - argparser.add_argument('-u', '--username', help='username', required=False) - argparser.add_argument('-p', '--password', help='password', required=False) - argparser.add_argument('-v', '--verify', action='store_true', help='verify', required=False, default=False) + argparser.add_argument("input_test_json", help="Input Test JSON file") + argparser.add_argument("-u", "--username", help="username", required=False) + argparser.add_argument("-p", "--password", help="password", required=False) + argparser.add_argument("-v", "--verify", action="store_true", help="verify", required=False, default=False) args = argparser.parse_args() session_id = None @@ -1467,27 +1415,29 @@ def finalize(self): # User specified a username but not a password, so ask import getpass + password = getpass.getpass("Password: ") if username and password: try: print("Accessing the Login page") r = requests.get(BaseConnector._get_phantom_base_url() + "login", verify=verify, timeout=MSTEAMS_DEFAULT_TIMEOUT) - csrftoken = r.cookies['csrftoken'] + csrftoken = r.cookies["csrftoken"] data = dict() - data['username'] = username - data['password'] = password - data['csrfmiddlewaretoken'] = csrftoken + data["username"] = username + data["password"] = password + data["csrfmiddlewaretoken"] = csrftoken headers = dict() - headers['Cookie'] = 'csrftoken={}'.format(csrftoken) - headers['Referer'] = BaseConnector._get_phantom_base_url() + 'login' + headers["Cookie"] = "csrftoken={}".format(csrftoken) + headers["Referer"] = BaseConnector._get_phantom_base_url() + "login" print("Logging into Platform to get the session id") - r2 = requests.post(BaseConnector._get_phantom_base_url() + "login", verify=verify, data=data, - headers=headers, timeout=MSTEAMS_DEFAULT_TIMEOUT) - session_id = r2.cookies['sessionid'] + r2 = requests.post( + BaseConnector._get_phantom_base_url() + "login", verify=verify, data=data, headers=headers, timeout=MSTEAMS_DEFAULT_TIMEOUT + ) + session_id = r2.cookies["sessionid"] except Exception as e: print("Unable to get session id from the platfrom. Error: {}".format(str(e))) sys.exit(1) @@ -1505,7 +1455,7 @@ def finalize(self): connector.print_progress_message = True if session_id is not None: - in_json['user_session_token'] = session_id + in_json["user_session_token"] = session_id ret_val = connector._handle_action(json.dumps(in_json), None) print(json.dumps(json.loads(ret_val), indent=4)) diff --git a/microsoftteams_consts.py b/microsoftteams_consts.py index 153b0be..f97520e 100644 --- a/microsoftteams_consts.py +++ b/microsoftteams_consts.py @@ -12,79 +12,84 @@ # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # either express or implied. See the License for the specific language governing permissions # and limitations under the License. -MSTEAMS_PHANTOM_SYS_INFO_URL = '/system_info' -MSTEAMS_PHANTOM_ASSET_INFO_URL = '/asset/{asset_id}' -MSTEAMS_LOGIN_BASE_URL = 'https://login.microsoftonline.com' -MSTEAMS_SERVER_TOKEN_URL = '/{tenant_id}/oauth2/v2.0/token' -MSTEAMS_AUTHORIZE_URL = '/{tenant_id}/oauth2/v2.0/authorize?client_id={client_id}&redirect_uri={redirect_uri}' \ - '&response_type={response_type}&state={state}&scope={scope}' -MSTEAMS_ADMIN_CONSENT_URL = '/{tenant_id}/adminconsent?client_id={client_id}&redirect_uri={redirect_uri}&state={state}' -MSTEAMS_MSGRAPH_API_BASE_URL = 'https://graph.microsoft.com/v1.0' -MSTEAMS_MSGRAPH_BETA_API_BASE_URL = 'https://graph.microsoft.com/beta' -MSTEAMS_MSGRAPH_SELF_ENDPOINT = '/me' -MSTEAMS_MSGRAPH_GROUPS_ENDPOINT = '/groups' -MSTEAMS_MSGRAPH_TEAMS_ENDPOINT = '/groups?$filter=resourceProvisioningOptions/Any(x:x eq \'Team\')' -MSTEAMS_MSGRAPH_LIST_USERS_ENDPOINT = '/users' -MSTEAMS_MSGRAPH_LIST_CHANNELS_ENDPOINT = '/teams/{group_id}/channels' -MSTEAMS_MSGRAPH_SEND_MSG_ENDPOINT = '/teams/{group_id}/channels/{channel_id}/messages' -MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT = '/me/chats' -MSTEAMS_MSGRAPH_LIST_ME_ENDPOINT = '/me' -MSTEAMS_MSGRAPH_LIST_USER_CHATS_ENDPOINT = '/users/{user_id}/chats' -MSTEAMS_MSGRAPH_SEND_DIRECT_MSG_ENDPOINT = '/chats/{chat_id}/messages' -MSTEAMS_MSGRAPH_CALENDER_EVENT_ENDPOINT = '/me/calendar/events' -MSTEAMS_MSGRAPH_ONLINE_MEETING_ENDPOINT = '/me/onlineMeetings' -MSTEAMS_TC_FILE = 'oauth_task.out' +MSTEAMS_PHANTOM_SYS_INFO_URL = "/system_info" +MSTEAMS_PHANTOM_ASSET_INFO_URL = "/asset/{asset_id}" +MSTEAMS_LOGIN_BASE_URL = "https://login.microsoftonline.com" +MSTEAMS_SERVER_TOKEN_URL = "/{tenant_id}/oauth2/v2.0/token" +MSTEAMS_AUTHORIZE_URL = ( + "/{tenant_id}/oauth2/v2.0/authorize?client_id={client_id}&redirect_uri={redirect_uri}" + "&response_type={response_type}&state={state}&scope={scope}" +) +MSTEAMS_ADMIN_CONSENT_URL = "/{tenant_id}/adminconsent?client_id={client_id}&redirect_uri={redirect_uri}&state={state}" +MSTEAMS_MSGRAPH_API_BASE_URL = "https://graph.microsoft.com/v1.0" +MSTEAMS_MSGRAPH_BETA_API_BASE_URL = "https://graph.microsoft.com/beta" +MSTEAMS_MSGRAPH_SELF_ENDPOINT = "/me" +MSTEAMS_MSGRAPH_GROUPS_ENDPOINT = "/groups" +MSTEAMS_MSGRAPH_TEAMS_ENDPOINT = "/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')" +MSTEAMS_MSGRAPH_LIST_USERS_ENDPOINT = "/users" +MSTEAMS_MSGRAPH_LIST_CHANNELS_ENDPOINT = "/teams/{group_id}/channels" +MSTEAMS_MSGRAPH_SEND_MSG_ENDPOINT = "/teams/{group_id}/channels/{channel_id}/messages" +MSTEAMS_MSGRAPH_LIST_CHATS_ENDPOINT = "/me/chats" +MSTEAMS_MSGRAPH_LIST_ME_ENDPOINT = "/me" +MSTEAMS_MSGRAPH_LIST_USER_CHATS_ENDPOINT = "/users/{user_id}/chats" +MSTEAMS_MSGRAPH_SEND_DIRECT_MSG_ENDPOINT = "/chats/{chat_id}/messages" +MSTEAMS_MSGRAPH_CALENDER_EVENT_ENDPOINT = "/me/calendar/events" +MSTEAMS_MSGRAPH_ONLINE_MEETING_ENDPOINT = "/me/onlineMeetings" +MSTEAMS_TC_FILE = "oauth_task.out" MSTEAMS_TC_STATUS_SLEEP = 3 MSTEAMS_AUTHORIZE_WAIT_TIME = 15 -MSTEAMS_TOKEN_NOT_AVAILABLE_MSG = 'Token not available. Please run test connectivity first.' -MSTEAMS_BASE_URL_NOT_FOUND_MSG = 'Phantom Base URL not found in System Settings. ' \ - 'Please specify this value in System Settings.' -MSTEAMS_TEST_CONNECTIVITY_FAILED_MSG = 'Test connectivity failed' -MSTEAMS_TEST_CONNECTIVITY_PASSED_MSG = 'Test connectivity passed' -MSTEAMS_ADMIN_CONSENT_MSG = 'Please hit the mentioned URL in another tab of browser to authorize the user and provide the admin consent: ' -MSTEAMS_ADMIN_CONSENT_FAILED_MSG = 'Admin consent not received' -MSTEAMS_ADMIN_CONSENT_PASSED_MSG = 'Admin consent Received' -MSTEAMS_AUTHORIZE_USER_MSG = 'Please authorize user in a separate tab using URL' -MSTEAMS_AUTHORIZE_WAIT_MSG = 'Waiting for authorization to complete' -MSTEAMS_AUTHORIZE_TROUBLESHOOT_MSG = 'If authorization URL fails to communicate with your SOAR instance, check whether you have: '\ - ' 1. Specified the Web Redirect URL of your App -- The Redirect URL should be /result . '\ - ' 2. Configured the base URL of your SOAR Instance at Administration -> Company Settings -> Info' -MSTEAMS_CODE_RECEIVED_MSG = 'Code Received' -MSTEAMS_MAKING_CONNECTION_MSG = 'Making Connection...' -MSTEAMS_REST_URL_NOT_AVAILABLE_MSG = 'Rest URL not available. Error: {error}' -MSTEAMS_OAUTH_URL_MSG = 'Using OAuth URL:' -MSTEAMS_GENERATING_ACCESS_TOKEN_MSG = 'Generating access token' -MSTEAMS_CURRENT_USER_INFO_MSG = 'Getting info about the current user to verify token' -MSTEAMS_GOT_CURRENT_USER_INFO_MSG = 'Got current user info' -MSTEAMS_INVALID_CHANNEL_MSG = 'Channel {channel_id} does not belongs to group {group_id}' -MSTEAMS_TOKEN_EXPIRED_MSG = 'Current access token has expired. New one will be generated.' -MSTEAMS_TOKEN_EXPIRED_MARKER = 'the token is expired' -MSTEAMS_TOKEN_GENERATED_MSG = 'New access token successfully generated.' -MSTEAMS_STATE_FILE_CORRUPT_ERROR = "Error occurred while loading the state file due to it's unexpected format. " \ +MSTEAMS_TOKEN_NOT_AVAILABLE_MSG = "Token not available. Please run test connectivity first." +MSTEAMS_BASE_URL_NOT_FOUND_MSG = "Phantom Base URL not found in System Settings. " "Please specify this value in System Settings." +MSTEAMS_TEST_CONNECTIVITY_FAILED_MSG = "Test connectivity failed" +MSTEAMS_TEST_CONNECTIVITY_PASSED_MSG = "Test connectivity passed" +MSTEAMS_ADMIN_CONSENT_MSG = "Please hit the mentioned URL in another tab of browser to authorize the user and provide the admin consent: " +MSTEAMS_ADMIN_CONSENT_FAILED_MSG = "Admin consent not received" +MSTEAMS_ADMIN_CONSENT_PASSED_MSG = "Admin consent Received" +MSTEAMS_AUTHORIZE_USER_MSG = "Please authorize user in a separate tab using URL" +MSTEAMS_AUTHORIZE_WAIT_MSG = "Waiting for authorization to complete" +MSTEAMS_AUTHORIZE_TROUBLESHOOT_MSG = ( + "If authorization URL fails to communicate with your SOAR instance, check whether you have: " + " 1. Specified the Web Redirect URL of your App -- The Redirect URL should be /result . " + " 2. Configured the base URL of your SOAR Instance at Administration -> Company Settings -> Info" +) +MSTEAMS_CODE_RECEIVED_MSG = "Code Received" +MSTEAMS_MAKING_CONNECTION_MSG = "Making Connection..." +MSTEAMS_REST_URL_NOT_AVAILABLE_MSG = "Rest URL not available. Error: {error}" +MSTEAMS_OAUTH_URL_MSG = "Using OAuth URL:" +MSTEAMS_GENERATING_ACCESS_TOKEN_MSG = "Generating access token" +MSTEAMS_CURRENT_USER_INFO_MSG = "Getting info about the current user to verify token" +MSTEAMS_GOT_CURRENT_USER_INFO_MSG = "Got current user info" +MSTEAMS_INVALID_CHANNEL_MSG = "Channel {channel_id} does not belongs to group {group_id}" +MSTEAMS_TOKEN_EXPIRED_MSG = "Current access token has expired. New one will be generated." +MSTEAMS_TOKEN_EXPIRED_MARKER = "the token is expired" +MSTEAMS_TOKEN_GENERATED_MSG = "New access token successfully generated." +MSTEAMS_STATE_FILE_CORRUPT_ERROR = ( + "Error occurred while loading the state file due to it's unexpected format. " "Resetting the state file with the default format. Please test the connectivity." -MSTEAMS_JSON_GROUP_ID = 'group_id' -MSTEAMS_JSON_CHANNEL_ID = 'channel_id' -MSTEAMS_JSON_CHAT_ID = 'chat_id' -MSTEAMS_JSON_USER_ID = 'user_id' -MSTEAMS_JSON_USER_FILTER = 'user' -MSTEAMS_JSON_CHAT_TYPE_FILTER = 'chat_type' -MSTEAMS_JSON_MSG = 'message' -MSTEAMS_JSON_SUBJECT = 'subject' -MSTEAMS_JSON_CALENDAR = 'add_calendar_event' -MSTEAMS_JSON_DESCRIPTION = 'description' -MSTEAMS_JSON_START_TIME = 'start_time' -MSTEAMS_JSON_END_TIME = 'end_time' -MSTEAMS_JSON_ATTENDEES = 'attendees' -MSTEAMS_CONFIG_TENANT_ID = 'tenant_id' -MSTEAMS_CONFIG_CLIENT_ID = 'client_id' -MSTEAMS_TOKEN_STRING = 'token' -MSTEAMS_STATE_IS_ENCRYPTED = 'is_encrypted' -MSTEAMS_ACCESS_TOKEN_STRING = 'access_token' -MSTEAMS_REFRESH_TOKEN_STRING = 'refresh_token' -MSTEAMS_CONFIG_CLIENT_SECRET = 'client_secret' # pragma: allowlist secret -MSTEAMS_CONFIG_TIMEZONE = 'timezone' -MSTEAMS_CONFIG_SCOPE = 'scope' -MSTEAMS_NEXT_LINK_STRING = '@odata.nextLink' +) +MSTEAMS_JSON_GROUP_ID = "group_id" +MSTEAMS_JSON_CHANNEL_ID = "channel_id" +MSTEAMS_JSON_CHAT_ID = "chat_id" +MSTEAMS_JSON_USER_ID = "user_id" +MSTEAMS_JSON_USER_FILTER = "user" +MSTEAMS_JSON_CHAT_TYPE_FILTER = "chat_type" +MSTEAMS_JSON_MSG = "message" +MSTEAMS_JSON_SUBJECT = "subject" +MSTEAMS_JSON_CALENDAR = "add_calendar_event" +MSTEAMS_JSON_DESCRIPTION = "description" +MSTEAMS_JSON_START_TIME = "start_time" +MSTEAMS_JSON_END_TIME = "end_time" +MSTEAMS_JSON_ATTENDEES = "attendees" +MSTEAMS_CONFIG_TENANT_ID = "tenant_id" +MSTEAMS_CONFIG_CLIENT_ID = "client_id" +MSTEAMS_TOKEN_STRING = "token" +MSTEAMS_STATE_IS_ENCRYPTED = "is_encrypted" +MSTEAMS_ACCESS_TOKEN_STRING = "access_token" +MSTEAMS_REFRESH_TOKEN_STRING = "refresh_token" +MSTEAMS_CONFIG_CLIENT_SECRET = "client_secret" # pragma: allowlist secret +MSTEAMS_CONFIG_TIMEZONE = "timezone" +MSTEAMS_CONFIG_SCOPE = "scope" +MSTEAMS_NEXT_LINK_STRING = "@odata.nextLink" MSTEAMS_DEFAULT_TIMEOUT = 30 MSTEAMS_VALID_CHAT_TYPES = ["oneOnOne", "group", "meeting", "unknownFutureValue"] diff --git a/microsoftteams_view.py b/microsoftteams_view.py index c2fdd3a..9a6efc7 100644 --- a/microsoftteams_view.py +++ b/microsoftteams_view.py @@ -13,7 +13,7 @@ # either express or implied. See the License for the specific language governing permissions # and limitations under the License. def _get_ctx_result(provides, result): - """ Function that parse data. + """Function that parse data. :param provides: action name :param result: result @@ -26,22 +26,22 @@ def _get_ctx_result(provides, result): summary = result.get_summary() data = result.get_data() - ctx_result['param'] = param + ctx_result["param"] = param if summary: - ctx_result['summary'] = summary + ctx_result["summary"] = summary if not data: - ctx_result['data'] = {} + ctx_result["data"] = {} return ctx_result - ctx_result['action'] = provides - ctx_result['data'] = data + ctx_result["action"] = provides + ctx_result["data"] = data return ctx_result def display_view(provides, all_app_runs, context): - """ Function that display flows. + """Function that display flows. :param provides: action name :param all_app_runs: all_app_runs @@ -49,7 +49,7 @@ def display_view(provides, all_app_runs, context): :return: html page name """ - context['results'] = results = [] + context["results"] = results = [] for summary, action_results in all_app_runs: for result in action_results: ctx_result = _get_ctx_result(provides, result) @@ -61,7 +61,7 @@ def display_view(provides, all_app_runs, context): def display_meeting(provides, all_app_runs, context): - """ Function that display flows. + """Function that display flows. :param provides: action name :param all_app_runs: all_app_runs @@ -69,7 +69,7 @@ def display_meeting(provides, all_app_runs, context): :return: html page name """ - context['results'] = results = [] + context["results"] = results = [] for summary, action_results in all_app_runs: for result in action_results: ctx_result = _get_ctx_result(provides, result) From e331954cd302fa0e92b9c42be4df734223fc57a9 Mon Sep 17 00:00:00 2001 From: splunk-soar-connectors-admin Date: Tue, 22 Oct 2024 21:43:42 +0000 Subject: [PATCH 06/10] Update README.md --- README.md | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2b8cd0..ff07b0f 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ # Microsoft Teams Publisher: Splunk -Connector Version: 2.5.2 +Connector Version: 2.6.2 Product Vendor: Microsoft Product Name: Teams Product Version Supported (regex): ".\*" -Minimum Product Version: 6.1.1 +Minimum Product Version: 6.2.2 This app integrates with Microsoft Teams to support various generic and investigative actions @@ -67,6 +67,7 @@ This app requires creating an app in the Azure Active Directory. | Channel.ReadBasic.All | list channels | Read channel names and channel descriptions, on behalf of the signed-in user. | No | ChannelMessage.Send | send message | Allows an app to send channel messages in Microsoft Teams, on behalf of the signed-in user. | No | GroupMember.Read.All | list groups, list teams | Allows the app to list groups, read basic group properties and read membership of all groups the signed-in user has access to. | Yes + | Chat.ReadWrite | read and send chat messages | Allows the app to read and send messages in chats on behalf of the signed-in user. | No | After making these changes, click **Add permissions** at the bottom of the screen, then @@ -219,6 +220,9 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION [get admin consent](#action-get-admin-consent) - Get the admin consent for a non-admin user [list users](#action-list-users) - List all users [send message](#action-send-message) - Send a message to a channel of a group +[list chats](#action-list-chats) - List chats for the authenticated user +[send direct message](#action-send-direct-message) - Send a direct message to a user +[send chat message](#action-send-chat-message) - Send a message to a specific chat [list channels](#action-list-channels) - Lists all channels of a group [list groups](#action-list-groups) - List all Azure Groups [list teams](#action-list-teams) - List all Microsoft Teams @@ -376,6 +380,144 @@ action_result.message | string | | Message sent summary.total_objects | numeric | | 1 summary.total_objects_successful | numeric | | 1 +## action: 'list chats' +List chats for the authenticated user + +Type: **investigate** +Read only: **True** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**user** | optional | Filter chats containing a specific user (by email or user id) | string | +**chat_type** | optional | Filter chats by type (e.g., oneOnOne, group, meeting) | string | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.user | string | | +action_result.data.\*.id | string | `ms teams chat id` | +action_result.data.\*.topic | string | | +action_result.data.\*.createdDateTime | string | | +action_result.data.\*.lastUpdatedDateTime | string | | +action_result.data.\*.chatType | string | | +action_result.data.\*.webUrl | string | `url` | +action_result.data.\*.tenantId | string | | +action_result.data.\*.viewpoint.isHidden | boolean | | +action_result.data.\*.viewpoint.lastMessageReadDateTime | string | | +action_result.data.\*.onlineMeetingInfo.joinWebUrl | string | `url` | +action_result.data.\*.onlineMeetingInfo.conferenceId | string | | +action_result.data.\*.onlineMeetingInfo.joinUrl | string | `url` | +action_result.data.\*.onlineMeetingInfo.phones | string | | +action_result.data.\*.onlineMeetingInfo.quickDial | string | | +action_result.data.\*.onlineMeetingInfo.tollFreeNumbers | string | | +action_result.data.\*.onlineMeetingInfo.tollNumber | string | | +action_result.summary.total_chats | numeric | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + +## action: 'send direct message' +Send a direct message to a user + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**user_id** | required | ID of the user to send a direct message to | string | `ms teams user id` +**message** | required | Message content to send | string | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.user_id | string | `ms teams user id` | +action_result.parameter.message | string | | +action_result.data.\*.id | string | `ms teams message id` | +action_result.data.\*.chatId | string | `ms teams chat id` | +action_result.data.\*.createdDateTime | string | | +action_result.data.\*.deletedDateTime | string | | +action_result.data.\*.lastModifiedDateTime | string | | +action_result.data.\*.lastEditedDateTime | string | | +action_result.data.\*.etag | string | | +action_result.data.\*.importance | string | | +action_result.data.\*.locale | string | | +action_result.data.\*.messageType | string | | +action_result.data.\*.replyToId | string | `ms teams message id` | +action_result.data.\*.subject | string | | +action_result.data.\*.summary | string | | +action_result.data.\*.webUrl | string | `url` | +action_result.data.\*.from.user.id | string | `ms teams user id` | +action_result.data.\*.from.user.displayName | string | | +action_result.data.\*.body.content | string | | +action_result.data.\*.body.contentType | string | | +action_result.data.\*.attachments.\*.content | string | | +action_result.data.\*.attachments.\*.contentType | string | | +action_result.data.\*.attachments.\*.contentUrl | string | `url` | +action_result.data.\*.attachments.\*.id | string | `ms teams attachment id` | +action_result.data.\*.attachments.\*.name | string | | +action_result.data.\*.attachments.\*.teamsAppId | string | | +action_result.data.\*.attachments.\*.thumbnailUrl | string | `url` | +action_result.data.\*.channelIdentity.channelId | string | `ms teams channel id` | +action_result.data.\*.channelIdentity.teamId | string | `ms teams team id` | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + +## action: 'send chat message' +Send a message to a specific chat + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**chat_id** | required | ID of the chat to send the message to | string | `ms teams chat id` +**message** | required | Message content to send | string | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.chat_id | string | `ms teams chat id` | +action_result.parameter.message | string | | +action_result.data.\*.id | string | `ms teams message id` | +action_result.data.\*.chatId | string | `ms teams chat id` | +action_result.data.\*.createdDateTime | string | | +action_result.data.\*.deletedDateTime | string | | +action_result.data.\*.lastModifiedDateTime | string | | +action_result.data.\*.lastEditedDateTime | string | | +action_result.data.\*.etag | string | | +action_result.data.\*.importance | string | | +action_result.data.\*.locale | string | | +action_result.data.\*.messageType | string | | +action_result.data.\*.replyToId | string | `ms teams message id` | +action_result.data.\*.subject | string | | +action_result.data.\*.summary | string | | +action_result.data.\*.webUrl | string | `url` | +action_result.data.\*.from.user.id | string | `ms teams user id` | +action_result.data.\*.from.user.displayName | string | | +action_result.data.\*.body.content | string | | +action_result.data.\*.body.contentType | string | | +action_result.data.\*.attachments.\*.content | string | | +action_result.data.\*.attachments.\*.contentType | string | | +action_result.data.\*.attachments.\*.contentUrl | string | `url` | +action_result.data.\*.attachments.\*.id | string | `ms teams attachment id` | +action_result.data.\*.attachments.\*.name | string | | +action_result.data.\*.attachments.\*.teamsAppId | string | | +action_result.data.\*.attachments.\*.thumbnailUrl | string | `url` | +action_result.data.\*.channelIdentity.channelId | string | `ms teams channel id` | +action_result.data.\*.channelIdentity.teamId | string | `ms teams team id` | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + ## action: 'list channels' Lists all channels of a group @@ -543,7 +685,7 @@ action_result.data.\*.attendees.\*.status.time | string | | 0001-01-01T00:00: action_result.data.\*.attendees.\*.type | string | | required action_result.data.\*.body.content | string | | action_result.data.\*.body.contentType | string | | html -action_result.data.\*.bodyPreview | string | | .........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtYT +action_result.data.\*.bodyPreview | string | | .........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtY action_result.data.\*.changeKey | string | | 07XhOkNngkCkqoNfY+k/jQAFHNmDaQ== action_result.data.\*.createdDateTime | string | | 2022-04-22T10:51:54.9092527Z action_result.data.\*.end.dateTime | string | | 2022-04-23T12:40:00.0000000 From 03fb8e8206ab15924d6a70143383c560bd16696f Mon Sep 17 00:00:00 2001 From: grokas Date: Fri, 25 Oct 2024 14:25:04 -0400 Subject: [PATCH 07/10] PAPP-34866 release notes added --- release_notes/unreleased.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index fbcb2fd..5c394d2 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1 +1,5 @@ **Unreleased** + +* Added 'list chats' action to get all chats for user with filtering options [PAPP-34866] +* Added 'send direct message' action to send a direct message to a particular user [PAPP-34866] +* Added 'send chat message' action to send a message to a chat [PAPP-34866] \ No newline at end of file From ef5aa22e36f0f09cc73a90d93b0ab0911bc647ca Mon Sep 17 00:00:00 2001 From: grokas Date: Fri, 25 Oct 2024 15:10:11 -0400 Subject: [PATCH 08/10] PAPP-34866 wording improvements --- microsoftteams.json | 14 +++++++------- microsoftteams_connector.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/microsoftteams.json b/microsoftteams.json index f8d9562..ad99c3f 100644 --- a/microsoftteams.json +++ b/microsoftteams.json @@ -728,18 +728,18 @@ }, { "action": "list chats", - "description": "List chats for the authenticated user", + "description": "List chats for authenticated user", "type": "investigate", "identifier": "list_chats", "read_only": true, "parameters": { "user": { - "description": "Filter chats containing a specific user (by email or user id)", + "description": "Filter chats containing specific user (by email or user id)", "data_type": "string", "order": 0 }, "chat_type": { - "description": "Filter chats by type (e.g., oneOnOne, group, meeting)", + "description": "Filter chats by type", "data_type": "string", "value_list": ["oneOnOne", "group", "meeting", "unknownFutureValue"], "order": 1 @@ -868,7 +868,7 @@ "read_only": false, "parameters": { "user_id": { - "description": "ID of the user to send a direct message to", + "description": "ID of the user to send direct message to", "data_type": "string", "required": true, "primary": true, @@ -1046,13 +1046,13 @@ }, { "action": "send chat message", - "description": "Send a message to a specific chat", + "description": "Send a message to specific chat", "type": "generic", "identifier": "send_chat_message", "read_only": false, "parameters": { "chat_id": { - "description": "ID of the chat to send the message to", + "description": "ID of the chat to send message to", "data_type": "string", "required": true, "primary": true, @@ -2021,7 +2021,7 @@ "data_path": "action_result.data.*.bodyPreview", "data_type": "string", "example_values": [ - ".........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtY" + ".........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtYT" ] }, { diff --git a/microsoftteams_connector.py b/microsoftteams_connector.py index bc15cce..b24bd30 100644 --- a/microsoftteams_connector.py +++ b/microsoftteams_connector.py @@ -1169,12 +1169,12 @@ def _handle_list_chats(self, param): def _send_chat_message(self, action_result, chat_id, message): """This function is used to send a message to a chat. - :param action_result: Object of ActionResult class - :param chat_id: ID of the chat + :param action_result: ActionResult object + :param chat_id: ID of desired chat :param message: Message to be sent - :return: status success/failure, response + :return: status success/failure """ - endpoint = f"/chats/{chat_id}/messages" + endpoint = MSTEAMS_MSGRAPH_SEND_DIRECT_MSG_ENDPOINT.format(chat_id=chat_id) data = {"body": {"contentType": "html", "content": message}} From b53f577a8e88e9820e2abdeb702ed3bfeba9435b Mon Sep 17 00:00:00 2001 From: splunk-soar-connectors-admin Date: Fri, 25 Oct 2024 19:13:29 +0000 Subject: [PATCH 09/10] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ff07b0f..98f9b70 100644 --- a/README.md +++ b/README.md @@ -220,9 +220,9 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION [get admin consent](#action-get-admin-consent) - Get the admin consent for a non-admin user [list users](#action-list-users) - List all users [send message](#action-send-message) - Send a message to a channel of a group -[list chats](#action-list-chats) - List chats for the authenticated user +[list chats](#action-list-chats) - List chats for authenticated user [send direct message](#action-send-direct-message) - Send a direct message to a user -[send chat message](#action-send-chat-message) - Send a message to a specific chat +[send chat message](#action-send-chat-message) - Send a message to specific chat [list channels](#action-list-channels) - Lists all channels of a group [list groups](#action-list-groups) - List all Azure Groups [list teams](#action-list-teams) - List all Microsoft Teams @@ -381,7 +381,7 @@ summary.total_objects | numeric | | 1 summary.total_objects_successful | numeric | | 1 ## action: 'list chats' -List chats for the authenticated user +List chats for authenticated user Type: **investigate** Read only: **True** @@ -389,8 +389,8 @@ Read only: **True** #### Action Parameters PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS --------- | -------- | ----------- | ---- | -------- -**user** | optional | Filter chats containing a specific user (by email or user id) | string | -**chat_type** | optional | Filter chats by type (e.g., oneOnOne, group, meeting) | string | +**user** | optional | Filter chats containing specific user (by email or user id) | string | +**chat_type** | optional | Filter chats by type | string | #### Action Output DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES @@ -427,7 +427,7 @@ Read only: **False** #### Action Parameters PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS --------- | -------- | ----------- | ---- | -------- -**user_id** | required | ID of the user to send a direct message to | string | `ms teams user id` +**user_id** | required | ID of the user to send direct message to | string | `ms teams user id` **message** | required | Message content to send | string | #### Action Output @@ -469,7 +469,7 @@ summary.total_objects | numeric | | summary.total_objects_successful | numeric | | ## action: 'send chat message' -Send a message to a specific chat +Send a message to specific chat Type: **generic** Read only: **False** @@ -477,7 +477,7 @@ Read only: **False** #### Action Parameters PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS --------- | -------- | ----------- | ---- | -------- -**chat_id** | required | ID of the chat to send the message to | string | `ms teams chat id` +**chat_id** | required | ID of the chat to send message to | string | `ms teams chat id` **message** | required | Message content to send | string | #### Action Output @@ -685,7 +685,7 @@ action_result.data.\*.attendees.\*.status.time | string | | 0001-01-01T00:00: action_result.data.\*.attendees.\*.type | string | | required action_result.data.\*.body.content | string | | action_result.data.\*.body.contentType | string | | html -action_result.data.\*.bodyPreview | string | | .........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtY +action_result.data.\*.bodyPreview | string | | .........................................................................................................................................\\r\\nJoin Teams Meeting\\r\\nen-US\\r\\nhttps://teams.microsoft.com/l/meetup-join/19%3ameeting_ZDljMDhjMDAtNWI2Yy00MGUxLWExYjUtYT action_result.data.\*.changeKey | string | | 07XhOkNngkCkqoNfY+k/jQAFHNmDaQ== action_result.data.\*.createdDateTime | string | | 2022-04-22T10:51:54.9092527Z action_result.data.\*.end.dateTime | string | | 2022-04-23T12:40:00.0000000 From fa815704e63f64ec48b396ceca14104ec46b4e95 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 26 Oct 2024 10:50:21 -0700 Subject: [PATCH 10/10] Release notes for version 2.6.2 --- release_notes/2.6.2.md | 3 +++ release_notes/unreleased.md | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 release_notes/2.6.2.md diff --git a/release_notes/2.6.2.md b/release_notes/2.6.2.md new file mode 100644 index 0000000..4b53867 --- /dev/null +++ b/release_notes/2.6.2.md @@ -0,0 +1,3 @@ +* Added 'list chats' action to get all chats for user with filtering options [PAPP-34866] +* Added 'send direct message' action to send a direct message to a particular user [PAPP-34866] +* Added 'send chat message' action to send a message to a chat [PAPP-34866] \ No newline at end of file diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index 5c394d2..fbcb2fd 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1,5 +1 @@ **Unreleased** - -* Added 'list chats' action to get all chats for user with filtering options [PAPP-34866] -* Added 'send direct message' action to send a direct message to a particular user [PAPP-34866] -* Added 'send chat message' action to send a message to a chat [PAPP-34866] \ No newline at end of file