Jamf Pro コンテキスト ログを収集する

以下でサポートされています。

このドキュメントでは、LambdaEventBridge スケジュールを使用して AWS S3 を使用して Jamf Pro コンテキスト ログ(デバイスとユーザーのコンテキスト)を(Google Security Operations)に取り込む方法について説明します。

始める前に

  • Google SecOps インスタンス
  • Jamf Pro テナントへの特権アクセス
  • AWS(S3、IAM、Lambda、EventBridge)への特権アクセス

Jamf API ロールを構成する

  1. Jamf ウェブ UI にログインします。
  2. [設定] > [システム] セクション > [API のロールとクライアント] に移動します。
  3. [API Roles] タブを選択します。
  4. [New] をクリックします。
  5. API ロールの表示名(context_role など)を入力します。
  6. [Jamf Pro API ロールの権限] で、権限の名前を入力し、メニューから選択します。

    • コンピュータの一覧表
    • モバイル デバイスのインベントリ
  7. [保存] をクリックします。

Jamf API クライアントを構成する

  1. Jamf Pro で、[Settings > System section > API roles and clients] に移動します。
  2. [APl クライアント] タブを選択します。
  3. [New] をクリックします。
  4. API クライアントの表示名(context_client など)を入力します。
  5. [API Roles] フィールドに、前に作成した context_role ロールを追加します。
  6. [アクセス トークンの有効期間] に、アクセス トークンが有効になるまでの時間を秒単位で入力します。
  7. [保存] をクリックします。
  8. [編集] をクリックします。
  9. [API クライアントを有効にする] をクリックします。
  10. [保存] をクリックします。

Jamf クライアント シークレットを構成する

  1. Jamf Pro で、新しく作成した API クライアントに移動します。
  2. [Generate Client Secret] をクリックします。
  3. 確認画面で、[シークレットを作成] をクリックします。
  4. 次のパラメータを安全な場所に保存します。
    • ベース URL: https://<your>.jamfcloud.com
    • クライアント ID: UUID。
    • クライアント シークレット: 値は 1 回だけ表示されます。

Google SecOps 用に AWS S3 バケットと IAM を構成する

  1. バケットの作成のユーザーガイドに沿って、Amazon S3 バケットを作成します。
  2. 後で参照できるように、バケットの名前リージョンを保存します(例: jamfpro)。
  3. IAM ユーザーの作成のユーザーガイドに沿って、ユーザーを作成します。
  4. 作成したユーザーを選択します。
  5. [セキュリティ認証情報] タブを選択します。
  6. [アクセスキー] セクションで [アクセスキーを作成] をクリックします。
  7. [ユースケース] で [サードパーティ サービス] を選択します。
  8. [次へ] をクリックします。
  9. 省略可: 説明タグを追加します。
  10. [アクセスキーを作成] をクリックします。
  11. [CSV ファイルをダウンロード] をクリックし、[アクセスキー] と [シークレット アクセスキー] を保存して、今後の参照に備えます。
  12. [完了] をクリックします。
  13. [権限] タブを選択します。
  14. [権限ポリシー] セクションで [権限を追加] をクリックします。
  15. [権限を追加] を選択します。
  16. [ポリシーを直接アタッチする] を選択します。
  17. AmazonS3FullAccess ポリシーを検索して選択します。
  18. [次へ] をクリックします。
  19. [権限を追加] をクリックします。

S3 アップロードの IAM ポリシーとロールを構成する

  1. ポリシー JSON(別のバケット名を入力した場合は jamfpro を置き換えます):

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowPutJamfObjects",
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::jamfpro/*"
        }
      ]
    }
    
  2. AWS コンソール > IAM > ポリシー > ポリシーの作成 > [JSON] タブ に移動します。

  3. ポリシーをコピーして貼り付けます。

  4. [次へ> ポリシーを作成] をクリックします。

  5. [IAM] > [ロール] > [ロールの作成] > [AWS サービス] > [Lambda] に移動します。

  6. 新しく作成したポリシーを関連付けます。

  7. ロールに「WriteJamfToS3Role」という名前を付けて、[ロールを作成] をクリックします。

Lambda 関数を作成する

  1. AWS コンソールで、[Lambda] > [Functions] > [Create function] に移動します。
  2. [Author from scratch] をクリックします。
  3. 次の構成情報を提供してください。
設定
名前 jamf_pro_to_s3
ランタイム Python 3.13
アーキテクチャ x86_64
権限 WriteJamfToS3Role
  1. 関数を作成したら、[コード] タブを開き、スタブを削除して次のコード(jamf_pro_to_s3.py)を入力します。

    import os
    import io
    import json
    import gzip
    import time
    import logging
    from datetime import datetime, timezone
    
    import boto3
    import requests
    
    log = logging.getLogger()
    log.setLevel(logging.INFO)
    
    BASE_URL = os.environ.get("JAMF_BASE_URL", "").rstrip("/")
    CLIENT_ID = os.environ.get("JAMF_CLIENT_ID")
    CLIENT_SECRET = os.environ.get("JAMF_CLIENT_SECRET")
    S3_BUCKET = os.environ.get("S3_BUCKET")
    S3_PREFIX = os.environ.get("S3_PREFIX", "jamf-pro/context/")
    PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "200"))
    
    SECTIONS = [
        "GENERAL",
        "HARDWARE",
        "OPERATING_SYSTEM",
        "USER_AND_LOCATION",
        "DISK_ENCRYPTION",
        "SECURITY",
        "EXTENSION_ATTRIBUTES",
        "APPLICATIONS",
        "CONFIGURATION_PROFILES",
        "LOCAL_USER_ACCOUNTS",
        "CERTIFICATES",
        "SERVICES",
        "PRINTERS",
        "SOFTWARE_UPDATES",
        "GROUP_MEMBERSHIPS",
        "CONTENT_CACHING",
        "STORAGE",
        "FONTS",
        "PACKAGE_RECEIPTS",
        "PLUGINS",
        "ATTACHMENTS",
        "LICENSED_SOFTWARE",
        "IBEACONS",
        "PURCHASING",
    ]
    
    s3 = boto3.client("s3")
    
    def _now_iso():
        return datetime.now(timezone.utc).isoformat()
    
    def get_token():
        """OAuth2 client credentials > access_token"""
        url = f"{BASE_URL}/api/oauth/token"
        data = {
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
        }
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        r = requests.post(url, data=data, headers=headers, timeout=30)
        r.raise_for_status()
        j = r.json()
        return j["access_token"], int(j.get("expires_in", 1200))
    
    def fetch_page(token: str, page: int):
        """GET /api/v1/computers-inventory with sections & pagination"""
        url = f"{BASE_URL}/api/v1/computers-inventory"
        params = [("page", page), ("page-size", PAGE_SIZE)] + [("section", s) for s in SECTIONS]
        hdrs = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
        r = requests.get(url, params=params, headers=hdrs, timeout=60)
        r.raise_for_status()
        return r.json()
    
    def to_context_event(item: dict) -> dict:
        inv = item.get("inventory", {}) or {}
        general = inv.get("general", {}) or {}
        hardware = inv.get("hardware", {}) or {}
        osinfo = inv.get("operatingSystem", {}) or {}
        loc = inv.get("location", {}) or inv.get("userAndLocation", {}) or {}
    
        computer = {
            "udid": general.get("udid") or hardware.get("udid"),
            "deviceName": general.get("name") or general.get("deviceName"),
            "serialNumber": hardware.get("serialNumber") or general.get("serialNumber"),
            "model": hardware.get("model") or general.get("model"),
            "osVersion": osinfo.get("version") or general.get("osVersion"),
            "osBuild": osinfo.get("build") or general.get("osBuild"),
            "macAddress": hardware.get("macAddress"),
            "alternateMacAddress": hardware.get("wifiMacAddress"),
            "ipAddress": general.get("ipAddress"),
            "reportedIpV4Address": general.get("reportedIpV4Address"),
            "reportedIpV6Address": general.get("reportedIpV6Address"),
            "modelIdentifier": hardware.get("modelIdentifier"),
            "assetTag": general.get("assetTag"),
        }
    
        user_block = {
            "userDirectoryID": loc.get("username") or loc.get("userDirectoryId"),
            "emailAddress": loc.get("emailAddress"),
            "realName": loc.get("realName"),
            "phone": loc.get("phone") or loc.get("phoneNumber"),
            "position": loc.get("position"),
            "department": loc.get("department"),
            "building": loc.get("building"),
            "room": loc.get("room"),
        }
    
        return {
            "webhook": {"name": "api.inventory"},
            "event_type": "ComputerInventory",
            "event_action": "snapshot",
            "event_timestamp": _now_iso(),
            "event_data": {
                "computer": {k: v for k, v in computer.items() if v not in (None, "")},
                **{k: v for k, v in user_block.items() if v not in (None, "")},
            },
            "_jamf": {
                "id": item.get("id"),
                "inventory": inv,
            },
        }
    
    def write_ndjson_gz(objs, when: datetime):
        buf = io.BytesIO()
        with gzip.GzipFile(filename="-", mode="wb", fileobj=buf, mtime=int(time.time())) as gz:
            for obj in objs:
                line = json.dumps(obj, separators=(",", ":")) + "\n"
                gz.write(line.encode("utf-8"))
        buf.seek(0)
    
        prefix = S3_PREFIX.strip("/") + "/" if S3_PREFIX else ""
        key = f"{prefix}{when:%Y/%m/%d}/jamf_pro_context_{int(when.timestamp())}.ndjson.gz"
        s3.put_object(Bucket=S3_BUCKET, Key=key, Body=buf.getvalue())
        return key
    
    def lambda_handler(event, context):
        assert BASE_URL and CLIENT_ID and CLIENT_SECRET and S3_BUCKET, "Missing required env vars"
    
        token, _ttl = get_token()
        page = 0
        total = 0
        batch = []
        now = datetime.now(timezone.utc)
    
        while True:
            payload = fetch_page(token, page)
            results = payload.get("results") or payload.get("computerInventoryList") or []
            if not results:
                break
    
            for item in results:
                batch.append(to_context_event(item))
                total += 1
    
            if len(batch) >= 5000:
                key = write_ndjson_gz(batch, now)
                log.info("wrote %s records to s3://%s/%s", len(batch), S3_BUCKET, key)
                batch = []
    
            if len(results) < PAGE_SIZE:
                break
            page += 1
    
        if batch:
            key = write_ndjson_gz(batch, now)
            log.info("wrote %s records to s3://%s/%s", len(batch), S3_BUCKET, key)
    
        return {"ok": True, "count": total}
    
  2. [構成> 環境変数> 編集> 新しい環境変数を追加] に移動します。

  3. 次の環境変数を入力します。プレースホルダは、実際の値に置き換えます。

    環境変数

    キー
    S3_BUCKET jamfpro
    S3_PREFIX jamf-pro/context/
    AWS_REGION 地域を選択する
    JAMF_CLIENT_ID Jamf クライアント ID を入力する
    JAMF_CLIENT_SECRET Jamf クライアント シークレットを入力する
    JAMF_BASE_URL Jamf URL を入力し、https://<your>.jamfcloud.com<your> を置き換えます。
    PAGE_SIZE 200
  4. 関数が作成されたら、そのページにとどまるか、[Lambda > 関数 > your-function] を開きます。

  5. [CONFIGURATION] タブを選択します。

  6. [全般設定] パネルで、[編集] をクリックします。

  7. [Timeout] を [5 minutes (300 seconds)] に変更し、[Save] をクリックします。

EventBridge スケジュールを作成する

  1. Amazon EventBridge > Scheduler > スケジュールの作成に移動します。
  2. 次の構成の詳細を入力します。
    • 定期的なスケジュール: 料金(1 hour)。
    • ターゲット: Lambda 関数。
    • 名前: jamfpro-context-schedule-1h
  3. [スケジュールを作成] をクリックします。

Jamf Pro コンテキスト ログを取り込むように Google SecOps でフィードを構成する

  1. [SIEM 設定] > [フィード] に移動します。
  2. [+ 新しいフィードを追加] をクリックします。
  3. [フィード名] フィールドに、フィードの名前を入力します(例: Jamf Pro Context logs)。
  4. [ソースタイプ] として [Amazon S3 V2] を選択します。
  5. [ログタイプ] として [Jamf Pro コンテキスト] を選択します。
  6. [次へ] をクリックします。
  7. 次の入力パラメータの値を指定します。
    • S3 URI: バケット URI
      • s3://jamfpro/jamf-pro/context/
        • jamfpro は、バケットの実際の名前に置き換えます。
    • Source deletion options: 必要に応じて削除オプションを選択します。
    • ファイルの最大経過日数: 指定した日数以内に変更されたファイルを含めます。デフォルトは 180 日です。
    • アクセスキー ID: s3 バケットにアクセスできるユーザー アクセスキー。
    • シークレット アクセスキー: s3 バケットにアクセスできるユーザーのシークレット キー。
    • アセットの名前空間: アセットの名前空間
    • Ingestion labels: このフィードのイベントに適用されるラベル。
  8. [次へ] をクリックします。
  9. [Finalize] 画面で新しいフィードの設定を確認し、[送信] をクリックします。

さらにサポートが必要な場合 コミュニティ メンバーや Google SecOps のプロフェッショナルから回答を得ることができます。