Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom ask and mirroring #29401

Merged
merged 44 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b4c4d30
send notification
jbabazadeh Jul 19, 2023
5b70733
send notification
jbabazadeh Jul 19, 2023
a7f8b46
zoom ask
jbabazadeh Aug 4, 2023
3851aa4
zoom async
jbabazadeh Aug 7, 2023
a817cd8
zoom async
jbabazadeh Aug 7, 2023
fe9eecc
zoom readme
jbabazadeh Aug 16, 2023
53d7ef5
secret token for mirroring
jbabazadeh Aug 20, 2023
6aa658e
send-notification command+ mirroring
jbabazadeh Aug 28, 2023
bbeebf0
unit tests
jbabazadeh Aug 31, 2023
1dbfab4
merge master
jbabazadeh Aug 31, 2023
44ca064
fix
jbabazadeh Aug 31, 2023
fc8504b
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 3, 2023
fb8a49b
add context cache
jbabazadeh Sep 5, 2023
faa1566
add context cache
jbabazadeh Sep 5, 2023
33f0472
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 5, 2023
7f7eb34
zoom ask tests
jbabazadeh Sep 5, 2023
6009071
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 5, 2023
f4e4782
remove unnecessary debug logs
jbabazadeh Sep 5, 2023
a0f7cc3
remove debug logs
jbabazadeh Sep 5, 2023
926cdce
CR comments
jbabazadeh Sep 7, 2023
dee179e
CR comments
jbabazadeh Sep 7, 2023
454d82f
rn
jbabazadeh Sep 10, 2023
9e484eb
format
jbabazadeh Sep 10, 2023
571a8bd
zoomask tests
jbabazadeh Sep 11, 2023
2a16e3e
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 11, 2023
48f1020
cr
jbabazadeh Sep 11, 2023
d1de9e9
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 11, 2023
3c555b0
small fix
jbabazadeh Sep 11, 2023
1844d0c
known_words
jbabazadeh Sep 11, 2023
a32ba21
Apply suggestions from code review
jbabazadeh Sep 13, 2023
19ac8fa
Update ZoomAsk.yml
jbabazadeh Sep 13, 2023
e0ef7e5
Apply suggestions from code review
jbabazadeh Sep 20, 2023
ba952cd
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 20, 2023
6827b09
zoom basic authentication
jbabazadeh Sep 28, 2023
f59b72c
Merge branch 'master' into Zoom-Ask
jbabazadeh Sep 28, 2023
e06bc38
comments
jbabazadeh Oct 9, 2023
aa449e4
Merge branch 'master' into Zoom-Ask
jbabazadeh Oct 9, 2023
40028f9
readme+ merge from master
jbabazadeh Oct 19, 2023
ac5f3ce
Merge branch 'master' into Zoom-Ask
jbabazadeh Oct 31, 2023
36c1798
add certificate to uvicorn to support https
jbabazadeh Oct 31, 2023
0691839
Merge branch 'master' into Zoom-Ask
jbabazadeh Nov 1, 2023
9eb5e21
yml
jbabazadeh Nov 1, 2023
eccead0
readme
jbabazadeh Nov 1, 2023
3089d73
Merge branch 'master' into Zoom-Ask
jbabazadeh Nov 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 57 additions & 43 deletions Packs/ApiModules/Scripts/ZoomApiModule/ZoomApiModule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import demistomock as demisto # noqa: F401
import jwt
from CommonServerPython import * # noqa: F401
from datetime import timedelta
import dateparser
Expand All @@ -20,7 +19,8 @@
INVALID_CREDENTIALS = 'Invalid credentials. Please verify that your credentials are valid.'
INVALID_API_SECRET = 'Invalid API Secret. Please verify that your API Secret is valid.'
INVALID_ID_OR_SECRET = 'Invalid Client ID or Client Secret. Please verify that your ID and Secret is valid.'

INVALID_TOKEN = 'Invalid Authorization token. Please verify that your Bot ID and Bot Secret is valid.'
INVALID_BOT_ID = 'No Chatbot can be found with the given robot_jid value. Please verify that your Bot JID is correct'
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
'''CLIENT CLASS'''


Expand All @@ -35,40 +35,53 @@ def __init__(
account_id: str | None = None,
client_id: str | None = None,
client_secret: str | None = None,
bot_client_id: str | None = None,
bot_client_secret: str | None = None,
verify=True,
proxy=False,
bot_jid: str | None = None,
):
super().__init__(base_url, verify, proxy)
self.api_key = api_key
self.api_secret = api_secret
self.account_id = account_id
self.client_id = client_id
self.client_secret = client_secret
is_jwt = (api_key and api_secret) and not (client_id and client_secret and account_id)
if is_jwt:
# the user has chosen to use the JWT authentication method (deprecated)
self.access_token: str | None = get_jwt_token(api_key, api_secret) # type: ignore[arg-type]
else:
# the user has chosen to use the OAUTH authentication method.
try:
self.access_token = self.get_oauth_token()
except Exception as e:
demisto.debug(f"Cannot get access token. Error: {e}")
self.access_token = None
self.bot_client_id = bot_client_id
self.bot_client_secret = bot_client_secret
self.bot_jid = bot_jid
try:
self.access_token, self.bot_access_token = self.get_oauth_token()
except Exception as e:
demisto.info(f"Cannot get access token. Error: {e}")
self.access_token = None
self.bot_access_token = None

def generate_oauth_token(self):
"""
Generate an OAuth Access token using the app credentials (AKA: client id and client secret) and the account id
Generate an OAuth Access token using the app credentials (AKA: client id and client secret) and the account id

:return: valid token
"""
:return: valid token
"""
token_res = self._http_request(method="POST", full_url=OAUTH_TOKEN_GENERATOR_URL,
params={"account_id": self.account_id,
"grant_type": "account_credentials"},
auth=(self.client_id, self.client_secret))
return token_res.get('access_token')

def get_oauth_token(self, force_gen_new_token=False):
def generate_oauth_client_token(self):
"""
Generate an OAuth Access token using the app credentials (AKA: client id and client secret) and the account id

:return: valid token
"""
token_res = self._http_request(method="POST", full_url=OAUTH_TOKEN_GENERATOR_URL,
params={"account_id": self.account_id,
"grant_type": "client_credentials"},
auth=(self.bot_client_id, self.bot_client_secret))
return token_res.get('access_token')
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved

def get_oauth_token(self, bot_client=False, client=False, force_gen_new_token=False):
"""
Retrieves the token from the server if it's expired and updates the global HEADERS to include it

Expand All @@ -79,10 +92,15 @@ def get_oauth_token(self, force_gen_new_token=False):
"""
now = datetime.now()
ctx = get_integration_context()
client_oauth_token = None
oauth_token = None

if not ctx or not ctx.get('token_info').get('generation_time', force_gen_new_token):
# new token is needed
oauth_token = self.generate_oauth_token()
if self.client_id and self.client_secret:
oauth_token = self.generate_oauth_token()
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
if self.bot_client_id and self.bot_client_secret:
client_oauth_token = self.generate_oauth_client_token()
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
ctx = {}
else:
if generation_time := dateparser.parse(
Expand All @@ -93,14 +111,19 @@ def get_oauth_token(self, force_gen_new_token=False):
time_passed = TOKEN_LIFE_TIME
if time_passed < TOKEN_LIFE_TIME:
# token hasn't expired
return ctx.get('token_info').get('oauth_token')
return ctx.get('token_info', {}).get('oauth_token'), ctx.get('token_info', {}).get('client_oauth_token')
else:
# token expired
oauth_token = self.generate_oauth_token()

ctx.update({'token_info': {'oauth_token': oauth_token, 'generation_time': now.strftime("%Y-%m-%dT%H:%M:%S")}})
# new token is needed
if self.client_id and self.client_secret:
oauth_token = self.generate_oauth_token()
if self.bot_client_id and self.bot_client_secret:
client_oauth_token = self.generate_oauth_client_token()

ctx.update({'token_info': {'oauth_token': oauth_token, 'client_oauth_token': client_oauth_token,
'generation_time': now.strftime("%Y-%m-%dT%H:%M:%S")}})
set_integration_context(ctx)
return oauth_token
return oauth_token, client_oauth_token

def error_handled_http_request(self, method, url_suffix='', full_url=None, headers=None,
auth=None, json_data=None, params=None, files=None, data=None,
Expand All @@ -115,30 +138,21 @@ def error_handled_http_request(self, method, url_suffix='', full_url=None, heade
auth=auth, json_data=json_data, params=params, files=files, data=data,
return_empty_response=return_empty_response, resp_type=resp_type, stream=stream)
except DemistoException as e:
if ('Invalid access token' in e.message
or "Access token is expired." in e.message):
self.access_token = self.generate_oauth_token()
headers = {'authorization': f'Bearer {self.access_token}'}
if any(message in e.message for message in ["Invalid access token",
"Access token is expired.",
"Invalid authorization token"]):
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
if url_suffix == '/im/chat/messages':
demisto.debug('generate new bot client token')
self.bot_access_token = self.generate_oauth_client_token()
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
headers = {'authorization': f'Bearer {self.bot_access_token}'}
else:
self.access_token = self.generate_oauth_token()
jbabazadeh marked this conversation as resolved.
Show resolved Hide resolved
headers = {'authorization': f'Bearer {self.access_token}'}
return super()._http_request(method=method, url_suffix=url_suffix, full_url=full_url, headers=headers,
auth=auth, json_data=json_data, params=params, files=files, data=data,
return_empty_response=return_empty_response, resp_type=resp_type, stream=stream)
else:
raise DemistoException(e.message)
raise DemistoException(e.message, url_suffix)


''' HELPER FUNCTIONS '''


def get_jwt_token(apiKey: str, apiSecret: str) -> str:
"""
Encode the JWT token given the api ket and secret
"""
now = datetime.now()
expire_time = int(now.strftime('%s')) + JWT_LIFETIME
payload = {
'iss': apiKey,

'exp': expire_time
}
encoded = jwt.encode(payload, apiSecret, algorithm='HS256')
return encoded
16 changes: 0 additions & 16 deletions Packs/ApiModules/Scripts/ZoomApiModule/ZoomApiModule_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,3 @@ def test_http_request___when_raising_invalid_token_message(mocker):
pass
assert m.call_count == 2
assert generate_token_mock.called


@freeze_time("1988-03-03T11:00:00")
def test_get_jwt_token__encoding_format_check():
"""
Given -

When -
creating a jwt token
Then -
Validate that the token is in the right format
"""
import ZoomApiModule
encoded_token = ZoomApiModule.get_jwt_token(apiKey="blabla", apiSecret="blabla")
# 124 is the expected token length based on parameters given
assert len(encoded_token) == 124
1 change: 1 addition & 0 deletions Packs/Zoom/.pack-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ignore=IM111

[known_words]
zoomapimodule
JWT

[file:Zoom.yml]
ignore=MR108
Expand Down
179 changes: 169 additions & 10 deletions Packs/Zoom/Integrations/Zoom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,81 @@ This integration was integrated and tested with version 2.0.0 of Zoom

| **Parameter** | **Description** | **Required** |
| --- | --- | --- |
| Server URL (e.g. '<https://api.zoom.us/v2/>') | | False |
| Account ID (OAuth) | | False |
| Client ID (OAuth) | | False |
| Client Secret (OAuth) | | False |
| API Key (JWT-Deprecated.) | This authentication method will be deprecated by Zoom in June 2023. | False |
| API Secret (JWT-Deprecated.) | This authentication method will be deprecated by Zoom in June 2023. | False |
| API Key (JWT-Deprecated.) | This authentication method will be deprecated by Zoom in June 2023. | False |
| API Secret (JWT-Deprecated.) | This authentication method will be deprecated by Zoom in June 2023. | False |
| Use system proxy settings | | False |
| Trust any certificate (not secure) | | False |
| `Server URL` (e.g., '<https://api.zoom.us/v2/>') | | True |
| `Account ID (OAuth)` | | True |
| `Client ID (OAuth)` | | True |
| `Client Secret (OAuth)` | | True |
| `Use system proxy settings` | | False |
| `Trust any certificate (not secure)` | | False |
| `Long running instance`| Enable in order to use zoom-ask and for mirroring. |False |
| `Listen Port`|Listener port number. |False|
| `Bot JID`| Zoom Bot app JID. | False|
| `Bot Client ID (OAuth)`| Zoom Bot app client ID. | False|
| `Bot Client Secret (OAuth)`| Zoom Bot app secret ID. | False|
| `Secret Token`| For mirroring, see [Configuring Secret Token](#secret-token). |False|
| `Mirroring` | Enable Incident Mirroring. See [how to configure the app](#secret-token). | False |



4. Click **Test** to validate the URLs, token, and connection.


### Server configuration

In the Server Configuration section, verify that the value for the instance.execute.external.`INTEGRATION-INSTANCE-NAME` key is set to true. If this key does not exist:
1. Click **+ Add Server Configuration**.
2. Add **instance.execute.external.`INTEGRATION-INSTANCE-NAME` ** and set the value to **true**. See the following reference article for further information.
- For Cortex XSOAR 6.x: `<CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME>`. For example, `https://my.demisto.live/instance/execute/zoom`. Note that the string `instance` does not refer to the name of your XSOAR instance, but rather is part of the URL.
- For Cortex XSOAR 8: `<ext-<CORTEX-XSOAR-URL>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME>`. For example, https://ext-dev-demisto.live/xsoar/instance/execute/zoom. Note that the string `instance` does not refer to the name of your XSOAR instance, but rather is part of the URL.


## Create Zoom ChatBOT app
1. Navigate to https://marketplace.zoom.us/.
2. Click **Develop** > **Build Team** > **Team Chat Apps**.
3. Enter the **App Name**.
4. Click **Create**.

![enter image description here](doc_files/create-team-chat-app.gif)
### Configure App Settings
Enter your Cortex XSOAR server URL in all Redirect URLS.

1. Click **Feature**> **Team Chat**.
In the Team Chat Subscription section under BOT endpoint URL add:
- For Cortex XSOAR 6.x: `<CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME>`. For example, `https://my.demisto.live/instance/execute/zoom`. Note that the string `instance` does not refer to the name of your Cortex XSOAR instance, but rather is part of the URL.
- For Cortex XSOAR 8: `<ext-<CORTEX-XSOAR-URL>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME>`. For example, https://ext-dev-demisto.live/xsoar/instance/execute/zoom. Note that the string `instance` does not refer to the name of your Cortex XSOAR instance, but rather is part of the URL.

![enter image description here](doc_files/bot_endpoint_url.gif)

1. Click **Scopes** > **+ Add Scopes** to add the following scope permissions.
| Scope Type | Scope Name |
| --- | --- |
| Team Chat | Enable Chatbot within Zoom Team Chat Client /imchat:bot |
| Team Chat | Send a team chat message to a Zoom Team Chat user or channel on behalf of a Chatbot /imchat:write:admin |
| Team Chat | View and manage all users' team chat channels /chat_channel:write:admin |
| User | View all user information /user:read:admin |
![enter image description here](doc_files/scope-premissions.png)

1. Click **Local Test** >**Add** to test your app and authorize your Cortex XSOAR app.
![enter image description here](doc_files/test-zoom-app.gif)

1. If mirroring is enabled in the integration configuration:
<a name="secret-token"></a>
1. Copy the **secret token** from the "Feature" page under the "Token" section and add it
to the system configuration.
![enter image description here](doc_files/zoom-token.png)

2. Configure Event Subscriptions.
1. In the "Feature" page
under the "General Features" section, enable "Event Subscriptions".
2. Click **+Add New Event Subscription**.
3. Enter the following information:
- Subscription name: Enter a name for this Event Subscription (e.g., "Send Message Sent").
- Event notification endpoint URL: Enter the Cortex XSOAR URL of your server (`CORTEX-XSOAR-URL`/instance/execute/`INTEGRATION-INSTANCE-NAME`) where you want to receive event notifications. This URL should handle incoming event data from Zoom. Make sure it's publicly accessible.
- Validate the URL: Just after setting up/configuration of the Cortex XSOAR side you can validate the URL.
- Add Events: Click **+Add Events**. Under Event types, select **Chat Message** and then select **Chat message sent**.
![enter image description here](doc_files/add-event.gif)


## Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
Expand Down Expand Up @@ -1838,3 +1900,100 @@ Searches chat messages or shared files between a user and an individual contact
>| 2023-05-22T08:24:14Z | None | a62636c8-b6c1-4135-9352-88ac61eafc31 | <[email protected]> | message | admin zoom | None | uJiZN-O7Rp6Jp_995FpZGg |
>| 2023-05-22T08:20:22Z | None | 4a59df4a-9668-46bd-bff2-3e1f3462ecc3 | <[email protected]> | my message | admin zoom | None | uJiZN-O7Rp6Jp_995FpZGg |

### send-notification

***
Sends messages from your Marketplace Chatbot app on Zoom to either an individual user or to a channel.

#### Base Command

`send-notification`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| to | The email address or user ID or member ID of the person to send a message. | Required |
| channel_id | The channel ID of the channel to send a message. | Optional |
| message | The message to be sent. Maximum of 1024 characters. | Required |
| visible_to_user | The UserID that allows a Chatbot to send a message to a group channel when it has only one designated person in that group channel to see the message. | Optional |
| zoom_ask | Whether to send the message as a JSON. | Optional |

#### Context Output

There is no context output for this command.

#### Command example

```!send-notification message=hi [email protected]```

#### Context Output

There is no context output for this command.

#### Human Readable Output

>### Message
>Message sent to Zoom successfully. Message ID is: 20230815153245201_BPK3S3S_aw1


## mirror-investigation

***
Mirrors the investigation between Zoom and the Cortex XSOAR War Room.

#### Base Command

`mirror-investigation`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| type | The mirroring type. Can be "all", which mirrors everything, "chat", which mirrors only chats (not commands), or "none", which stops all mirroring. Possible values are: all, chat, none. Default is all. | Optional |
| autoclose | Whether the channel is auto-closed when an investigation is closed. Can be "true" or "false". Default is "true". | Optional |
| direction | The mirroring direction. Can be "FromDemisto", "ToDemisto", or "Both". Default value is "Both". | Optional |
| channelName | The name of the channel. The default is "incident-&lt;incidentID&gt;". | Optional |

#### Context Output

There is no context output for this command.

#### Command Example

```!mirror-investigation direction="FromDemisto" channelName="example"```

#### Human Readable Output

> Investigation mirrored successfully, channel:example

### close-channel

***
Delete a mirrored Zoom channel.

#### Base Command

`close-channel`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| channel | The name of the channel to delete. If not provided, the mirrored investigation channel is deleted (if the channel exists). | Optional |
| channel_id | The ID of the channel to delete. If not provided, the mirrored investigation channel is deleted (if the channel exists). | Optional |

#### Context Output

There is no context output for this command.

#### Command Example

```
!close-channel channel=new-zoom-channel
```

#### Human Readable Output

> Channel successfully deleted.

Loading
Loading