def delete_channel(logger: Logger, connection: komand.connection, team_id: str, channel_id: str) -> bool: """ Deletes a channel for a given team :param logger: (logging.logger) :param connection: Object (komand.connection) :param team_id: String :param channel_id: String :return: boolean """ delete_channel_endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/teams/{team_id}/channels/{channel_id}" headers = connection.get_headers() logger.info(f"Deleting channel with: {delete_channel_endpoint}") result = requests.delete(delete_channel_endpoint, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause=f"Delete channel {channel_id} failed.", assistance=result.text) from e if not result.status_code == 204: raise PluginException( cause=f"Delete channel returned an unexpected result.", assistance=result.text) return True
def delete_group(logger: Logger, connection: komand.connection, group_name: str) -> bool: """ This will delete a group from Azure :param logger: object :param connection: object :param group_name: string :return: boolean """ group_id = get_group_id_from_name(logger, connection, group_name) endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups/{group_id}" headers = connection.get_headers() logger.info(f"Deleting group with: {endpoint}") result = requests.delete(endpoint, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause="Delete group failed", assistance=result.text) from e # https://docs.microsoft.com/en-us/graph/api/group-delete if result.status_code == 204: return True raise PluginException( cause= f"The server returned an unexpected result while deleting the '{group_name}' group.", assistance= "Please contact Rapid7 support with the following information:", data=result.text)
def send_html_message(logger: Logger, connection: komand.connection, message: str, team_id: str, channel_id: str) -> dict: """ Send HTML content as a message to Teams :param logger: object (logging.logger) :param connection: object (komand.connection) :param message: String (HTML) :param team_id: String :param channel_id: String :return: dict """ send_message_url = f"https://graph.microsoft.com/beta/teams/{team_id}/channels/{channel_id}/messages" logger.info(f"Sending message to: {send_message_url}") headers = connection.get_headers() body = {"body": {"contentType": "html", "content": message}} result = requests.post(send_message_url, headers=headers, json=body) try: result.raise_for_status() except Exception as e: raise PluginException(cause="Send message failed.", assistance=result.text) from e message = result.json() return message
def get_channels_from_microsoft(logger: Logger, connection: komand.connection, team_id: str, channel_name=None, explicit=False) -> list: """ This will get all channels available to a team from the Graph API If the channel_name is provided it will only return that channel or throw an error if that channel is not found :param logger: object (logging.logger) :param connection: (komand.connection) :param team_id: String :param channel_name: String :param explicit: boolean :return: list """ compiled_channel_name = None if channel_name: try: compiled_channel_name = re.compile(channel_name) except Exception as e: raise PluginException(cause=f"Channel Name {compiled_channel_name} was an invalid regular expression.", assistance=f"Please correct {compiled_channel_name}") from e # See if we are looking for a channel name exactly or not if explicit: channels_url = f"https://graph.microsoft.com/beta/{connection.tenant_id}/teams/{team_id}/channels?filter=displayName eq '{channel_name}'" else: channels_url = f"https://graph.microsoft.com/beta/{connection.tenant_id}/teams/{team_id}/channels" headers = connection.get_headers() channels_result = requests.get(channels_url, headers=headers) try: channels_result.raise_for_status() except Exception as e: raise PluginException(cause="Attempt to get channels failed.", assistance=channels_result.text) from e try: channels = channels_result.json().get("value") except Exception as e: raise PluginException(PluginException.Preset.INVALID_JSON) from e # Note: the channels endpoint does not paginate. Channels max out at 200 per team # All 200 will be returned in one list if channel_name: logger.info(f"Channel name: {channel_name}") for channel in channels: name = channel.get("displayName") logger.info(f"Checking channel: {name}") if compiled_channel_name.search(name): return [channel] else: raise PluginException(cause=f"Channel {channel_name} was not found.", assistance=f"Please verify {channel_name} is a valid channel for the team " f"with id: {team_id}") return channels
def create_group(logger: Logger, connection: komand.connection, group_name: str, group_description: str, group_nickname: str, mail_enabled: bool, owners: list, members: list) -> dict: """ This will create a group in Azure AD :param logger: object :param connection: object :param group_name: string :param group_description: string :param group_nickname: string :param mail_enabled: boolean :param owners: list :param members: list :return: object """ endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups" headers = connection.get_headers() payload = { "description": group_description, "displayName": group_name, "groupTypes": ["Unified"], "mailEnabled": mail_enabled, "mailNickname": group_nickname, "securityEnabled": False } if owners: owners_payload = create_user_paylaod(logger, connection, owners) payload["*****@*****.**"] = owners_payload if members: members_payload = create_user_paylaod(logger, connection, members) payload["*****@*****.**"] = members_payload logger.info(f"Creating group with: {endpoint}") result = requests.post(endpoint, json=payload, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause=f"Unable to create group: {group_name}", assistance=result.text) from e # https://docs.microsoft.com/en-us/graph/api/group-post-groups?view=graph-rest-1.0&tabs=http if result.status_code == 201: try: return result.json() except Exception as e: raise PluginException(PluginException.Preset.INVALID_JSON) from e raise PluginException( cause= f"Unexpected response from server when creating group {group_name}. Response code: " f"{result.status_code}.", assistance= "Please contact Rapid7 support with the following error information:", data=result.text)
def add_user_to_owners(logger: Logger, connection: komand.connection.Connection, group_id: str, user_id: str) -> bool: endpoint = f"https://graph.microsoft.com/beta/groups/{group_id}/owners/$ref" logger.info(f"Adding user to group owners with: {endpoint}") result = requests.post( endpoint, json={ "@odata.id": f"https://graph.microsoft.com/beta/users/{user_id}" }, headers=connection.get_headers(), ) if result.status_code == 204: logger.info("User was added successfully.") return True if result.status_code == 400: logger.info( "Unable to add user to group owners. The user is already one of a group owners." ) return True elif result.status_code == 401: raise PluginException( cause="Invalid credentials.", assistance="Please check that provided credentials are correct.", ) elif result.status_code == 403: raise PluginException( cause= "The account configured in your plugin connection is unauthorized to access this service.", assistance="Verify the permissions for your account and try again.", ) elif result.status_code == 404: raise PluginException( cause="Invalid username or group provided.", assistance= "Please check that provided username and group are correct.", ) try: result.raise_for_status() except Exception as e: raise PluginException( cause= f"Unable to add user to group owners:\nUser:{user_id}\nGroup:{group_id}.", assistance=f"{result.text}.", data=e, ) raise PluginException( cause= f"Unexpected response from server when adding user to group owners. Response code: {result.status_code}.", assistance= "Please contact Rapid7 support with the following error information:", data=result.text, )
def add_user_to_channel(logger: Logger, connection: komand.connection.Connection, group_id: str, channel_id: str, user_id: str) -> bool: endpoint = f"https://graph.microsoft.com/beta/teams/{group_id}/channels/{channel_id}/members/" logger.info(f"Adding user to channel with: {endpoint}") result = requests.post( endpoint, json={ "@odata.type": "#microsoft.graph.aadUserConversationMember", "roles": [], "*****@*****.**": f"https://graph.microsoft.com/beta/users/{user_id}" }, headers=connection.get_headers()) if result.status_code == 201: logger.info("User was added successfully.") return True if result.status_code == 400: logger.info( "Unable to add user to channel. The user has already been added to the channel." ) return True elif result.status_code == 401: raise PluginException( cause="Invalid credentials.", assistance="Please check that provided credentials are correct.") elif result.status_code == 403: raise PluginException( cause= "The account configured in your plugin connection is unauthorized to access this service.", assistance="Verify the permissions for your account and try again." ) elif result.status_code == 404: raise PluginException( cause="Invalid username, group or channel provided.", assistance= "Please check that provided username, group and channel are correct." ) try: result.raise_for_status() except Exception as e: raise PluginException( cause= f"Unable to add user to channel:\nUser:{user_id}\nChannel:{channel_id}.", assistance=f"{result.text}.", data=e) raise PluginException( cause= f"Unexpected response from server when adding user to channel. Response code: {result.status_code}.", assistance= "Please contact Rapid7 support with the following error information:", data=result.text)
def get_teams_from_microsoft(logger: Logger, connection: komand.connection, team_name=None) -> list: """ This will get teams from the Graph API. If a team_name is provided it will only return that team, or throw an error if that team is not found :param logger: object (logging.logger) :param connection: object (komand.connection) :param team_name: string :return: array of teams """ compiled_team_name = None if team_name: try: compiled_team_name = re.compile(team_name) except Exception as e: raise PluginException( cause= f"Team Name {team_name} was an invalid regular expression.", assistance=f"Please correct {team_name}") from e teams_url = "https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')" headers = connection.get_headers() teams_result = requests.get(teams_url, headers=headers) try: teams_result.raise_for_status() except Exception as e: raise PluginException(cause="Attempt to get teams failed.", assistance=teams_result.text) from e try: teams = teams_result.json().get("value") except Exception as e: raise PluginException(PluginException.Preset.INVALID_JSON) from e if team_name: logger.info(f"Team name: {team_name}") for team in teams: name = team.get("displayName") logger.info(f"Checking team: {name}") if compiled_team_name.search(name): return [team] else: raise PluginException( cause=f"Team {team_name} was not found.", assistance=f"Please verify {team_name} is a valid team name") return teams
def get_user_info(logger: Logger, connection: komand.connection, user_login: str) -> dict: """ This is used to get information about a user using the user login :param logger: object :param connection: object :param user_login: string :return: object (user information dictionary) """ endpoint = ( f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/users?$filter=userPrincipalName eq '{user_login}'" ) headers = connection.get_headers() logger.info(f"Getting user information from:\n{endpoint}") result = requests.get(endpoint, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause=f"Unable to get user {user_login}", assistance=result.text) from e logger.info(f"Getting user information return code:{result.status_code}") try: response_json = result.json() users = response_json.get("value") except Exception as e: raise PluginException( cause="Get user info returned an unexpected response.", assistance= "Please contact Rapid7 support with the following information:", data=result.text, ) from e try: user = users[0] except IndexError as e: raise PluginException( cause= "The server did not send back any results, but the get users call was successful.", assistance= f"Usually this indicates the user was not found.\nUser was {user_login}.\n", data=result.text, ) from e return user
def add_user_to_group(logger: Logger, connection: komand.connection, group_id: str, user_id: str) -> bool: """ This will add a user to a group :param logger: object :param connection: object :param group_id: string :param user_id: string :return: boolean """ endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups/{group_id}/members/$ref" headers = connection.get_headers() logger.info(f"Adding user with: {endpoint}") user_payload = { "@odata.id": f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/users/{user_id}" } result = requests.post(endpoint, json=user_payload, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException( cause= f"Unable to add user to group:\nUser:{user_id}\nGroup:{group_id}", assistance=result.text, ) from e # https://docs.microsoft.com/en-us/graph/api/group-post-members if result.status_code == 204: return True raise PluginException( cause= f"Unexpected response from server when adding user to group. Response code: " f"{result.status_code}.", assistance= "Please contact Rapid7 support with the following error information:", data=result.text, )
def get_group_id_from_name(logger: Logger, connection: komand.connection, group_name: str) -> str: """ This will take a group name and return its ID :param logger: object :param connection: object :param group_name: string :return: string """ endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups?$filter=displayName eq '{group_name}'" headers = connection.get_headers() logger.info(f"Getting group ID with: {endpoint}") result = requests.get(endpoint, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause="Get group ID failed.", assistance=result.text) from e try: result_json = result.json() results = result_json.get("value") except Exception as e: raise PluginException(PluginException.Preset.INVALID_JSON) from e try: result = results[0] except Exception as e: raise PluginException( cause= "Get group id was successful, but the server did not return any results.", assistance= f"This usually indicates the group was not found.\nGroup: {group_name}", data=result.text, ) from e return result.get("id")
def create_channel(logger: Logger, connection: komand.connection, team_id: str, channel_name: str, description: str) -> bool: """ Creates a channel for a given team :param logger: (logging.logger) :param connection: Object (komand.connection) :param team_id: String :param channel_name: String :param description: String :return: boolean """ create_channel_endpoint = f"https://graph.microsoft.com/beta/teams/{team_id}/channels" create_channel_paylaod = { "description": description, "displayName": channel_name } headers = connection.get_headers() logger.info(f"Creating channel with: {create_channel_endpoint}") result = requests.post(create_channel_endpoint, json=create_channel_paylaod, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException(cause=f"Create channel {channel_name} failed.", assistance=result.text) from e if not result.status_code == 201: raise PluginException( cause=f"Create channel returned an unexpected result.", assistance=result.text) return True
def remove_user_from_group(logger: Logger, connection: komand.connection, group_id: str, user_id: str) -> bool: """ Removes a user from a group :param logger: object :param connection: object :param group_id: string :param user_id: string :return: boolean """ endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups/{group_id}/members/{user_id}/$ref" headers = connection.get_headers() logger.info(f"Removing user with: {endpoint}") result = requests.delete(endpoint, headers=headers) try: result.raise_for_status() except Exception as e: raise PluginException( cause= f"Unable to remove user from group:\nUser:{user_id}\nGroup:{group_id}.", assistance=f"{result.text}.", ) from e # https://docs.microsoft.com/en-us/graph/api/group-post-members # 204 no content if result.status_code == 204: return True raise PluginException( cause= f"Unexpected response from server when removing user from group. Response code: " f"{result.status_code}.", assistance= "Please contact Rapid7 support with the following error information:", data=result.text, )
def get_teams_from_microsoft(logger: Logger, connection: komand.connection, team_name=None, explicit=True) -> list: """ This will get teams from the Graph API. If a team_name is provided it will only return that team, or throw an error if that team is not found :param logger: object (logging.logger) :param connection: object (komand.connection) :param team_name: string :param explicit: boolean :return: array of teams """ compiled_team_name = None if team_name: try: compiled_team_name = re.compile(team_name) except Exception as e: raise PluginException( cause= f"Team Name {team_name} was an invalid regular expression.", assistance=f"Please correct {team_name}") from e # See if we are looking for a team name exactly or not if explicit: teams_url = f"https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team') and displayName eq '{team_name}'" else: teams_url = f"https://graph.microsoft.com/beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')" headers = connection.get_headers() teams_result = requests.get(teams_url, headers=headers) try: teams_result.raise_for_status() except Exception as e: raise PluginException(cause="Attempt to get teams failed.", assistance=teams_result.text) from e try: teams = teams_result.json().get("value") except Exception as e: raise PluginException(PluginException.Preset.INVALID_JSON) from e nextlink = teams_result.json().get("@odata.nextLink") # If there's more than 20 teams, the results will come back paginated. while nextlink: try: new_teams = requests.get(nextlink, headers=headers) except Exception as e: raise PluginException( cause="Attempt to get paginated teams failed.", assistance=teams_result.text) from e nextlink = new_teams.json().get("@odata.nextLink", "") teams.extend(new_teams.json().get("value")) if team_name: logger.info(f"Team name: {team_name}") for team in teams: name = team.get("displayName") logger.info(f"Checking team: {name}") if compiled_team_name.search(name): return [team] else: raise PluginException( cause=f"Team {team_name} was not found.", assistance=f"Please verify {team_name} is a valid team name") return teams
def enable_teams_for_group(logger, connection, group_id): """ This will take a group ID and enable it in Teams :param logger: object :param connection: object :param group_id: string :return: boolean """ endpoint = f"https://graph.microsoft.com/v1.0/{connection.tenant_id}/groups/{group_id}/team" headers = connection.get_headers() payload = { "memberSettings": { "allowCreateUpdateChannels": True, "allowDeleteChannels": True, "allowAddRemoveApps": True, "allowCreateUpdateRemoveTabs": True, "allowCreateUpdateRemoveConnectors": True }, "guestSettings": { "allowCreateUpdateChannels": False, "allowDeleteChannels": False }, "messagingSettings": { "allowUserEditMessages": True, "allowUserDeleteMessages": True, "allowOwnerDeleteMessages": True, "allowTeamMentions": True, "allowChannelMentions": True }, "funSettings": { "allowGiphy": True, "giphyContentRating": "strict", "allowStickersAndMemes": True, "allowCustomMemes": True } } logger.info(f"Enabling team with: {endpoint}") result = requests.put(endpoint, json=payload, headers=headers) if result.status_code == 201: logger.info("Team was enabled successfully.") return True # https://docs.microsoft.com/en-us/graph/api/team-put-teams?view=graph-rest-1.0&tabs=http # This pattern is suggested by microsoft for i in range( 2, 6 ): # 2 to 5 - this is our second attempt, and python range is weird...thus 6 logger.info( f"Attempt to enable team failed. Status code: {result.status_code}\n" f"Sleeping for 10 seconds and trying again. Attempt number: {i}") sleep(10) result = requests.put(endpoint, json=payload, headers=headers) if result.status_code == 201: logger.info("Team was enabled successfully.") return True raise PluginException( cause=f"Could not enable Teams for group with ID: {group_id}.", assistance= "This may be due to a replication delay in Azure. Please try again later, or " "enable the team manually. If this problem persists, contact Rapid7 support with " "the following information:", data=f"Status Code: {result.status_code}\n" f"Response:\n{result.text}")