見出し画像

Zscaler Internet AccessのAPIを叩いてみよう

こんばんは、しがない美少女情シスでヤンス…へへへ、旦那ァ、こんな記事にたどり着くなんて、もの好きでヤンスね…。

はい、というわけで。(切り替え)

この記事は「corp-engr 情シスSlack(コーポレートエンジニア x 情シス)#1 Advent Calendar 2024」の20日目の記事になります。

ホッホッホ、良い子のみんな、インターネットセキュリティは好きかのう?(情報セキュリティサンタ)

…ということで、今回はSSE製品であるZscaler Internet Access(以後、ZIAと表記)のWebAPIを操作してみよう!というお話です。

現職はフルリモートワーク環境であり、インターネットアクセスへのセキュリティに対するニーズがとても高いです。
そのため、情報セキュリティ強化の一環としてZIAを導入・運用しています。

運用・管理を効率化するために、ZIAをGUI以外の手段でコントロールしてみよう、ということになり、手持ちの環境からWebAPIを叩いてみた記録になります。

ZIAのAPIまわりは日本語解説記事なんかもあんまり見当たらない(そんなにニーズも無いであろう)印象なので、半分は自分のための備忘録として書いております。
のこり半分はZIAの理解を深めたいユーザー方の参考になるような内容に…なるといいなぁ…なるよねきっと………うん…!と、思っています。
クリスマスシーズンのコンビニチキン感覚でお楽しみください。

事前準備

ZIAの認証仕様の確認

まず手始めに、ZIAのWebAPIリファレンスを読み解いてみましょう。

APIエンドポイントへHTTPリクエストを送受信するには、最初にZIAのAPIサーバーと認証セッションを確立する必要があります。
認証セッションの確立には、以下の2通りの方法が存在します。

  1. OAuth 2.0承認サーバーを用意する

  2. APIキーを使用してセッションを張る

1の方法は、まず自前で認証のためのサービスを用意する必要があります。
自前の認証方法を準備するのが大変な場合(承認サーバーは別途サブスクリプション契約が必要な場合アリ)、2の方法を採用することとなります。

今回は現在の手持ちの環境の中から運用時のユースケースを想定し、一番実行しやすい手段であるGAS(Google App Script)からHTTPリクエストを送受信してみました。
(作ったGASをAPIエンドポイントとして利用したりするため)

以降、基本的には上述のリファレンスガイドに沿った内容で進め、サンプルプログラムを実行するところまでを目標として進めてみます。

ZIAのベースURLを取得する

ZIAはリージョンや契約ごとにいくつかデータセンターが割り振られます。
データセンターごとにURLは異なっているので、自分が契約したデータセンターのURLを確認します。

例:zsapi.zscalerxxxx.net (xxxxの部分は実際の契約サーバー名で変動)

手っ取り早いのは管理コンソールへアクセスして、表示されたドメイン名を見てみればすぐに判別可能だと思います。
確認したベースURLはAPIエンドポイントへHTTPリクエストを送るときに必要なので、控えておきましょう。

ZIAのAPIキーを取得する

APIにアクセスするのに必要な、APIキーを取得します。
APIキーは以下の手順で確認可能です。

  1. ZIA管理ポータルにログインする

  2. 「管理」-「クラウド サービスAPIキーセキュリティ」にアクセス

  3. 「クラウド サービスAPIキー」タブに表示されているキーを確認

基本的に一契約一組織につき一つのAPIキーが発行されるようです。

Zscaler Internet access クラウド サービス APIキーについて ページより画像引用

GAS側の準備

GASでAPIを叩く場合、APIキーなどをスクリプトに直接ハードコーディングしたくないので、それらの情報はスクリプトプロパティとして保存し、コードとは分離します。

スクリプトプロパティを開いて、ここまでのステップで取得した情報と、のちの認証セッションを張る際に使用する管理者権限のBasic認証情報(ID/PW)などを格納していきます。

サンプルで使用する固定値のスクリプトプロパティの例

処理の実装

サンプルコード

一旦サンプルコードを貼ります。
その上で、実装時のポイントなどは後述します。
サンプル実行完了の目標は「urlLookup処理(Google,Youtube,Yahoo!のURLカテゴリーを調べる)をリクエストして結果を取得する」です。
なお、スクリプトのファイルはメイン処理、セッションまわりの処理、定数の格納の3つのスクリプトに分けてあります。

メイン処理

/**
 * 注記:ZIAに対してスクリプトからAPIリクエストを送るサンプル処理です
 * これをアレンジししてZIAに対するAPI処理を実装してください
 */


/**
 * ZscalerInternetAccessの情報取得処理
 */
function main() {
  // タイムスタンプ取得
  const timestamp = getTimestamp();
  // APIキー難読化
  const apiToken = obfuscateApiKey(timestamp, serviceApiKey);
  // APIセッション取得
  const apiSession = createAuthSession(timestamp, apiToken);
  
  // 情報取得処理
  const result = urlLookup(apiSession);

  if (result.getResponseCode !== 200) {
    // 異常終了したらAPIセッション終了
    deleteSession(apiSession);
  } else {
    // API処理を反映してセッション終了
    // statusActivate(apiSession); 構成変更(更新)が必要な処理の場合にのみコメントを外して実施
    deleteSession(apiSession);
  }
}

/**
 * メインのHTTPリクエスト送受信
 */
function urlLookup(session) {
  const url = `${apiURL}/urlLookup`;
  const options = {
    "method" : "post",
    "muteHttpExceptions" : true,
    "headers": {
      "Content-Type" : "application/json",
      "Cache-Control" : "no-cache",
      "Cookie" : session
    },
    "payload" : "[\"google.com\",\"youtube.com\",\"yahoo.co.jp\"]"
  }
  const response = UrlFetchApp.fetch(url, options);

  // 実行結果をコンソールに表示
  Logger.log(response.getResponseCode());
  Logger.log(response.getContentText());

  return response;
}

セッション関連処理

/**
 * Zscaler Internet Access APIセッション関連処理
 * APIセッションの開始・終了まわりのファンクション群
 */

/**
 * API用タイムスタンプ取得処理
 * UNIXタイム:ミリ秒13桁
 */
function getTimestamp() {
  // タイムスタンプ取得:UNIX時間でミリ秒単位の13桁の文字列
  const nowDate = Utilities.formatDate(new Date(), 'GMT', 'dd MMM yyyy HH:mm:ss z');
  const timestamp = Date.parse(nowDate).toFixed();

  // 戻り値:UNIXタイムミリ秒
  return timestamp;
}

/**
 * APIキー難読化処理
 * サンプル:https://help.zscaler.com/ja/zia/getting-started-zia-api#RetrieveAccessToken
 * Javascriptのサンプルコードを流用した難読化ファンクション
 */
function obfuscateApiKey(timestamp, key) {
  let high = timestamp.substring(timestamp.length - 6);
  let low = (parseInt(high) >> 1).toString();
  let apiKey = "";
  
  while (low.length < 6) {
    low = "0" + low; 
  };
  for (let i = 0; i < high.length; i++) {
    apiKey += key.charAt(parseInt(high.charAt(i)));
  }
  for (let j = 0; j < low.length; j++) {
    apiKey += key.charAt(parseInt(low.charAt(j)) + 2);
  }

  // 戻り値:難読化後のAPIキー
  return apiKey;
}

/**
 * API認証セッション取得
 * 認証セッションしてレスポンスヘッダーからJSESSIONIDを取得
 */
function createAuthSession(timestamp, key) {
  // リファレンス:https://help.zscaler.com/ja/zia/getting-started-zia-api
  const url = `${apiURL}/authenticatedSession`;
  const options = {
    "method" : "post",
    "muteHttpExceptions" : true,
    "headers": {
      "Content-Type" : "application/json",
      "Cache-Control" : "no-cache"
    },
    "payload" : JSON.stringify({
      "apiKey" : key,
      "username" : adminID,
      "password" : adminPW,
      "timestamp" : timestamp
    })
  }
  const response = UrlFetchApp.fetch(url, options);
  Logger.log("session start:" + response.getResponseCode()); // セッション開始リターンコード確認

  // ヘッダーからセッションID取得
  const header = response.getHeaders();
  const setCookie = header["Set-Cookie"]; // Set-Cookie取得(JSESSIONIDを含む)
  const jsessionID = setCookie.split(";",1) // splitして扶養箇所を削除
  const result = jsessionID[0];

  // 戻り値:JSESSIONID文字列(JSESSIONID=XXXXXXXX)
  return result;
}

/**
 * APIセッション終了処理
 * 使い終わったらセッションを閉じる
 */
function deleteSession(session) {
  // セッション削除エンドポイント
  const url = `${apiURL}/authenticatedSession`;
  const options = {
    "method" : "delete",
    "muteHttpExceptions" : true,
    "headers": {
      "Content-Type" : "application/json",
      "Cache-Control" : "no-cache",
      "Cookie" : session
    }
  }
  const response = UrlFetchApp.fetch(url, options);
  Logger.log("session end:" + response.getResponseCode()); // セッション終了リターンコード確認
}

定数の格納

/**
 * 定数等の設定値を取得
 */
// APIセッション用
const baseURL = PropertiesService.getScriptProperties().getProperty('BASE_URL'); // ZIAベースURL
const apiURL = baseURL + "/api/v1"; // APIエンドポイント用URL
const serviceApiKey = PropertiesService.getScriptProperties().getProperty('API_KEY'); // ZIAから取得したAPIキー
const adminID = PropertiesService.getScriptProperties().getProperty('ZIA_ADMIN_ID'); // 管理者ID
const adminPW = PropertiesService.getScriptProperties().getProperty('ZIA_ADMIN_PW'); // 管理者パスワード

ポイントの解説

APIキーを難読化する必要がある

リファレンスガイドの「2.認証してAPIセッションを作成します。」にあるとおり、APIキーを難読化するための関数を自前で実装する必要があります。
リファレンスにあるサンプルをコピーして一部記述を修正すればOKなので、お手本通りにコピペ改変しましょう。
難読化に用いるタイムスタンプは、「UNIX時間でミリ秒単位の13桁の文字列」を取得して使用します。

認証セッションを張る必要がある

ZIAのAPIを操作する際は、まず認証セッションを張り、認証セッションクッキーが有効な間だけAPIが操作可能となります。
こちらも「2.認証してAPIセッションを作成します。」の手順に従い、/authenticatedSessionへHTTPリクエストを送ります。
(このとき、管理者IDとパスワード、難読化に使用したタイムスタンプ、を組み合わせて認証する)

認証に成功したらレスポンスヘッダーの中に「JSESSIONID」から始まるセッションのIDが含まれて返ってきます。
このJSESSIONIDは他のAPI処理時に「現在有効化されているセッションのID」として使用するので、ヘッダーから切り出して保持しておきます。

セッションクッキーの記述について

注意点としては、APIリクエストに投げる情報としては"JSESSIONID=XXXXXXXX"という「JSESSIONIDも含む文字列」であるという点です。
(IDだけ抜き出して渡そうとしたら、当然ながらめちゃくちゃ弾かれました)

処理が終わったら認証セッションを終了する

使ったらお片付けするのは基本のお行儀ですね。
一応セッションは放っておいても時間切れで自然消滅しますが、ジャックされる可能性を減らしたりするため、張ったセッションは必ず終了するようにします。

リファレンスの読み解き方

リファレンスガイドも用意されているので、お目当ての処理を探して実装しましょう。
ただ、記述が若干読みづらく、「どのパラメータをどのような形式で指定したら良いのか?」がパッとわからないものも存在します。
サンプルのurlLookupへのPOSTも、パラメータはわかるけど記述する形式はどうすれば…?と戸惑いました。

リファレンスガイドにある「Try In Postman」ボタンをクリックすると、Postmanに投げることが出来る形式のJSONフォーマットのサンプルファイルが落とせます。
もうサンプルファイルの中身を見ちゃって、JSONの記述方法を直接確認するほうがわかりやすいです。

お手本となるJSON構文ファイルが落とせる

サンプル実行結果

ステータスコード200でsuccess、ZIAがプリセットで保持するURLカテゴリーの情報が返ってきました。

実行ログの一部

おわりに

ここまで書いておいてなんですが、「これって内容的にはCyber-sec+ Advent Calendar 2024に書いたほうが良かったのでは…!?うわあああ」となっておりました。(12月中旬時点で9割書いて気付き時既に遅し)
でも、製品はセキュリティ製品であっても、やりたい内容の本旨は「運用管理の効率化メソッド」ということで、情シス!あくまで情シスの範囲!ということで平にご容赦をーーー!!!


はい、ということでZIAをAPI経由で操作してみよう、サンプルの巻でした。

ZIAはSSEという製品の性質上、認証が他製品と比較して厳しめで操作も難しい印象でした。
(払い出したAPIトークンを渡したらおしまい、というタイプのSaaSを多く触っていたので、最初はそうしたお作法に戸惑いました)

それ以外は他製品のWebAPIと変わらずJSONとHTTPリクエストの書き方ががわかればリクエスト送信&レスポンス取得は可能なので、固有のお作法さえ分かってしまえばこっちのものですね。

リファレンスガイドにはサンプルコードとしてPHP、PowerShell、Python、Java、Javascript、Shell Scriptがあげられており、様々な言語やCLIインターフェースを通じて処理が可能です。
お手持ちの環境で操作・作業を効率化したい、定型的な作業を自動化したいという場合などに、触ってみると良いでしょう。


それにしてもAPIを叩けば叩くほど、「私はいったいあといくつAPIお作法を覚えるんだろうか…???」と思えてきます。
ただ、もうこの辺はChatGPTをはじめとした生成AIが人間のバディになって、人間は生成されたコードをレビュー・修正するのが主流になっていきつつありますね。
お作法を覚えるニーズも徐々に減っていきつつあります。

ただしAIが賢くなっても、今のところはレビューに人間の知識・スキル・経験などの情報が多分に必要となっています。
実行結果に責任を持てるのも人間だけです。
人間がしっかりしてないとAIもトンチキになっちゃうのは、当面の間は変わらないんだなぁ、と思います。

…何の話でしたっけ。


そんなこんなで、多分この先もSaaSに触れ続ける限り、延々とAPIの叩き方を覚えちゃ忘れ、忘れちゃ調べ、ショボショボする眼でリファレンスを読み、GPTさんとかに「婆さんや、あのエンドポイントを操作する方法を書いてくれんかのう…?」と尋ね続けるのでしょうね…。

いいなと思ったら応援しよう!