Zscaler Internet Access の Internal API（仮称）を触ってみる（ログイン編）

# 1. はじめに

みなさん、こんにちわ。ゼットスケーラーの大谷です。カスタマーサクセスマネージャを担当しています。

この記事は[Zscaler Advent Calendar 2024](https://qiita.com/advent-calendar/2024/zscaler)向けの記事です。

https://qiita.com/advent-calendar/2024/zscaler

この記事では Zscaler Intenet Access（ZIA）の Intenal API（仮称）について書いています。

これまでの記事はこちらからどうぞ。

[Zscaler Internet Access の Internal API（仮称）を触ってみる（紹介編）](https://qiita.com/Shojiro_Otani/items/2035bd094dcbfd0f0c04)

一番最初の記事に書きましたが、ここで紹介している Internal API（仮称）には注意点があります。

- Zscaler では一部の社内向けのツールでこの機能を利用していますが、社外向けには公式にサポートしていません。
- 実際に「Internal API」という名称も存在しません。この場で便宜的に付けた仮称です。
- APIリファレンスも存在しませんので、どのように動くのかという情報はありません。現状の動くままに利用する必要があります。
- 将来的に突然利用できなくなることがあります。
- 大量にアクセスしようとすると Zscaler のセキュリティ対策によってアクセスが制限されることがあります。

# 2. いきなりの最難関： 認証をどうするか？

Internal API（仮称）は GUI でのアクセスを擬似的に操作するためのものなので、GUI ではどのように認証を行っているのか見てみます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/03dbf59e-de1f-5cfb-7ad6-80a501467d7a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/74e8d95c-fd21-afee-bdd2-f8eca9090118.png)

GUI では`https://admin.<cloud name>.net/zsapi/v1/authenticatedSession` への POST でユーザ名 `username` やパスワード`password`を渡していることがわかります。このリクエストに対するレスポンスで Cookie が提供されています。

`apiKey`は一体何でしょう？それに`timestamp`はどうやら Unix タイムスタンプのようですが、どのように使われているのかここではわかりません。

# 3. では、公開 API ではどのように認証しているのか？

公開 API のほうでは主に3つの要素を使用して認証を行います。

1. ユーザ名
2. パスワード
3. 難読化処理した APIキー（[Cloud Service API Key](https://help.zscaler.com/zia/about-cloud-service-api-key) を現在の Unix タイムスタンプと組み合わせて文字列を難読化処理したものです）

詳しくは[こちら](https://help.zscaler.com/zia/getting-started-zia-api)の「Using ZIA Admin Credentials and API Key/Token」に載っています。

では、今回の Internal API（仮称）ではどうなるかというと、ZIA の公開 API はプロビジョニングチケットで有効化を依頼するすることで利用可能なものですし、APIキーを生成していなくても Admin Portal にアクセスできるはずです。試しに認証だけ公開 API にアクセスしてもうまくいきませんでした。そもそも公開 API と Internal API（仮称）は API のエンドポイントが異なります。

公開 API：`/api/v1/`
Internal API（仮称）：`/zsapi/v1/`

ということで、この認証をどうするかという問題はこの Internal API（仮称）を利用するうえで一番の問題です。（でした。）

# 4. 最初に試したこと

これは一度ブラウザで認証したセッションキーをコードのなかで利用するという方法です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/4c980a1f-1181-bef6-85b7-3ab4f7c5fd1b.png)

`https://admin.<cloud name>.net/zsapi/v1/authenticatedSession` に対する認証リクエストによって提供される Cookie から`JSESESSIONID`と`ZS_SESSION_CODE`の値を取り出して、コードのなかで利用します。

`Zs_custom_code`という新しい設定がありますが、ブラウザでアクセスするときに送っているのでこれもセットします。値は`ZS_SESSION_CODE`と同じで良いようです。


```python
import requests
import json

JSESSIONID = "ここにセット"
ZS_SESSION_CODE = "ここにセット"
Zs_custom_code = ZS_SESSION_CODE

# 送信する Cookie としてセット
cookies = {
    "JSESSIONID": JSESSIONID,
    "ZS_SESSION_CODE": ZS_SESSION_CODE
}

# リクエストで送信するヘッダー（ブラウザでのアクセスと同じものを適当に設定）
headers = {
    'Accept': '*/*',
    'Accept-Language': 'ja',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Content-Type': 'application/json;charset=UTF-8',
    'Origin': 'https://admin.zscalerthree.net',
    'Pragma': 'no-cache',
    'Referer': 'https://admin.zscalerthree.net/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
    'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'zs_custom_code': ZS_SESSION_CODE #ここにセット

}

# 例としてテナントに関する基本的な情報を提供するCompany Profileにアクセス
company_profile_url = "https://admin.zscalerthree.net/zsapi/v1/orgInformation"
company_profile_response = requests.get(company_profile_url, headers=headers, cookies=cookies)

print(company_profile_response.text)
```
実行みたらうまくいきました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/4327bc3f-0642-1e24-d514-3df207c8cbdb.png" width=50%>

# 5. 完全な自動化にトライしてみる

一度ブラウザで認証するのはどうも手間です。

ですのでいろいろ調べてみたところ、認証する前に実行されるスクリプトの中にどうやら API キーを難読化処理しているような関数`obfuscateApiKey`があることがわかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/b7e3ed3e-57c3-ab39-4147-774192bdb3d7.png)

この引数 `e` と `E` が何を示しているのかさらに調べてみたところ、認証する前にアクセスできるとあるスクリプトのなかに APIキーが埋め込まれていることがわかりました。（誰でもアクセスできる場所にありますが、念のため具体的には書かないことにします。この関数の名前で検索してみるとわかると思います。）つまり`e`は API キーで、公開 API と同じだとすれば `E`は Unix タイムスタンプかもしれません。

ということでこのようなコードを実行してみると・・

```python
import requests
import time
import json
import getpass

# ユーザ名を設定
username = "ユーザ名をセット"

# クラウド名
cloudname = "クラウド名をセット"

# API エンドポイント
api_endpoint = "/zsapi/v1"

# API キーを設定
e = "上記のとおり念のため公開は控えておきます。"

# パスワードの入力を求める
password = getpass.getpass("Enter your password: ")

# API キーの難読化処理のための関数
def obfuscate_api_key(e, E):
    # Eの最後の6文字を取得
    a = E[-6:]

    # aを整数に変換し、右シフト1した結果を文字列に変換
    A = str(int(a) >> 1)

    # Aが6文字になるようにゼロ埋め
    while len(A) < 6:
        A = "0" + A

    # eからインデックスを基に文字を抽出してTを構築
    T = ""
    for char in a:
        T += e[int(char)]

    for char in A:
        T += e[int(char) + 2]

    return T

# 現在のUnixタイムスタンプを取得
E = str(int(time.time() * 1000))

# API キーの難読化処理
api_key = obfuscate_api_key(e, E)

# リクエストデータの設定
data = {
    'apiKey': api_key,
    'username': username,
    'password': password,
    'timestamp': E
}

# 認証のためのURLとヘッダーの設定（ブラウザでのアクセスと同じものを適当に設定）
url = f'https://admin.{cloudname}{api_endpoint}/authenticatedSession'
headers = {
    'Accept': '*/*',
    'Accept-Language': 'ja',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Content-Type': 'application/json;charset=UTF-8',
    'Origin': f'https://admin.{cloudname}',
    'Pragma': 'no-cache',
    'Referer': f'https://admin.{cloudname}/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
    'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"'
}

# POSTリクエストを送信
response = requests.post(url, headers=headers, data=json.dumps(data))
# Cookie を辞書として取得
cookies = response.cookies.get_dict()  
# レスポンスから JSESSIONID を取り出す
jsessionid = cookies.get('JSESSIONID')
# レスポンスから ZS_SESSION_CODE を取り出す
zs_session_code = cookies.get('ZS_SESSION_CODE')
# リクエストヘッダーに Zs_custom_code（ZS_SESSION_CODEと同じ値）を追加する
headers['Zs_custom_code'] = zs_session_code
# 送信する Cookie としてセット
cookies = {
    "JSESSIONID": jsessionid,
    "ZS_SESSION_CODE": zs_session_code  
}

# 例としてテナントに関する基本的な情報を提供するCompany Profileにアクセス
company_profile_url = f"https://admin.{cloudname}{api_endpoint}/orgInformation"
company_profile_response = requests.get(company_profile_url, headers=headers, cookies=cookies)

print(company_profile_response.text)
```

実行してみたらうまくいきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622818/9f14ce01-9737-cff5-4ffa-e1a12a81fcf4.png)

# 6. 最後に

次はログのダウンロードをする方法について書いてみようと思っています。