Collecter les journaux de contexte Jamf Pro

Compatible avec :

Ce document explique comment ingérer les journaux de contexte Jamf Pro (contexte de l'appareil et de l'utilisateur) dans Google Security Operations à l'aide d'AWS S3 avec la planification Lambda et EventBridge.

Avant de commencer

  • Instance Google SecOps
  • Accès privilégié au locataire Jamf Pro
  • Accès privilégié à AWS (S3, IAM, Lambda, EventBridge)

Configurer le rôle de l'API Jamf

  1. Connectez-vous à l'interface utilisateur Web Jamf.
  2. Accédez à Paramètres > section "Système" > Rôles et clients de l'API.
  3. Sélectionnez l'onglet Rôles de l'API.
  4. Cliquez sur New (Nouveau).
  5. Saisissez un nom à afficher pour le rôle d'API (par exemple, context_role).
  6. Dans Droits du rôle d'API Jamf Pro, saisissez le nom d'un droit, puis sélectionnez-le dans le menu.

    • Inventaire des ordinateurs
    • Inventaire des appareils mobiles
  7. Cliquez sur Enregistrer.

Configurer le client API Jamf

  1. Dans Jamf Pro, accédez à Paramètres > section "Système" > Rôles et clients de l'API.
  2. Sélectionnez l'onglet Clients de l'API.
  3. Cliquez sur New (Nouveau).
  4. Saisissez un nom à afficher pour le client API (par exemple, context_client).
  5. Dans le champ Rôles d'API, ajoutez le rôle context_role que vous avez créé précédemment.
  6. Sous Durée de vie du jeton d'accès, saisissez la durée de validité des jetons d'accès en secondes.
  7. Cliquez sur Enregistrer.
  8. Cliquez sur Modifier.
  9. Cliquez sur Activer le client API.
  10. Cliquez sur Enregistrer.

Configurer le code secret du client Jamf

  1. Dans Jamf Pro, accédez au client API que vous venez de créer.
  2. Cliquez sur Générer le code secret du client.
  3. Sur l'écran de confirmation, cliquez sur Créer un secret.
  4. Enregistrez les paramètres suivants dans un emplacement sécurisé :
    • URL de base : https://<your>.jamfcloud.com
    • ID client : UUID.
    • Code secret du client : la valeur n'est affichée qu'une seule fois.

Configurer un bucket AWS S3 et IAM pour Google SecOps

  1. Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
  2. Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple, jamfpro).
  3. Créez un utilisateur en suivant ce guide de l'utilisateur : Créer un utilisateur IAM.
  4. Sélectionnez l'utilisateur créé.
  5. Sélectionnez l'onglet Informations d'identification de sécurité.
  6. Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
  7. Sélectionnez Service tiers comme Cas d'utilisation.
  8. Cliquez sur Suivant.
  9. Facultatif : Ajoutez une balise de description.
  10. Cliquez sur Créer une clé d'accès.
  11. Cliquez sur Télécharger le fichier CSV pour enregistrer la clé d'accès et la clé d'accès secrète pour référence ultérieure.
  12. Cliquez sur OK.
  13. Sélectionnez l'onglet Autorisations.
  14. Cliquez sur Ajouter des autorisations dans la section Règles relatives aux autorisations.
  15. Sélectionnez Ajouter des autorisations.
  16. Sélectionnez Joindre directement des règles.
  17. Recherchez et sélectionnez la règle AmazonS3FullAccess.
  18. Cliquez sur Suivant.
  19. Cliquez sur Ajouter des autorisations.

Configurer la stratégie et le rôle IAM pour les importations S3

  1. JSON de la règle (remplacez jamfpro si vous avez saisi un autre nom de bucket) :

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowPutJamfObjects",
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::jamfpro/*"
        }
      ]
    }
    
  2. Accédez à la console AWS> IAM> Policies> Create policy> onglet JSON.

  3. Copiez et collez le règlement.

  4. Cliquez sur Suivant > Créer une règle.

  5. Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.

  6. Associez la règle nouvellement créée.

  7. Nommez le rôle WriteJamfToS3Role, puis cliquez sur Créer un rôle.

Créer la fonction Lambda

  1. Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
  2. Cliquez sur Créer à partir de zéro.
  3. Fournissez les informations de configuration suivantes :
Paramètre Valeur
Nom jamf_pro_to_s3
Durée d'exécution Python 3.13
Architecture x86_64
Autorisations WriteJamfToS3Role
  1. Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et saisissez le code suivant (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. Accédez à Configuration > Variables d'environnement > Modifier > Ajouter une variable d'environnement.

  3. Saisissez les variables d'environnement suivantes en remplaçant par vos valeurs.

    Variables d'environnement

    Clé Exemple
    S3_BUCKET jamfpro
    S3_PREFIX jamf-pro/context/
    AWS_REGION Sélectionnez votre région
    JAMF_CLIENT_ID Saisissez l'ID client Jamf
    JAMF_CLIENT_SECRET Saisir le code secret du client Jamf
    JAMF_BASE_URL Saisissez l'URL Jamf, puis remplacez <your> dans https://<your>.jamfcloud.com.
    PAGE_SIZE 200
  4. Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre-fonction).

  5. Accédez à l'onglet Configuration.

  6. Dans le panneau Configuration générale, cliquez sur Modifier.

  7. Définissez le délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.

Créer une programmation EventBridge

  1. Accédez à Amazon EventBridge> Scheduler> Create schedule.
  2. Fournissez les informations de configuration suivantes :
    • Programmation périodique : tarif (1 hour).
    • Cible : votre fonction Lambda.
    • Nom : jamfpro-context-schedule-1h.
  3. Cliquez sur Créer la programmation.

Configurer un flux dans Google SecOps pour ingérer les journaux de contexte Jamf Pro

  1. Accédez à Paramètres SIEM> Flux.
  2. Cliquez sur + Ajouter un flux.
  3. Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple, Jamf Pro Context logs).
  4. Sélectionnez Amazon S3 V2 comme type de source.
  5. Sélectionnez Contexte Jamf Pro comme Type de journal.
  6. Cliquez sur Suivant.
  7. Spécifiez les valeurs des paramètres d'entrée suivants :
    • URI S3 : URI du bucket
      • s3://jamfpro/jamf-pro/context/
        • Remplacez jamfpro par le nom réel du bucket.
    • Options de suppression de la source : sélectionnez l'option de suppression de votre choix.
    • Âge maximal des fichiers : incluez les fichiers modifiés au cours des derniers jours. La valeur par défaut est de 180 jours.
    • ID de clé d'accès : clé d'accès utilisateur ayant accès au bucket S3.
    • Clé d'accès secrète : clé secrète de l'utilisateur ayant accès au bucket S3.
    • Espace de noms de l'élément : espace de noms de l'élément.
    • Libellés d'ingestion : libellé à appliquer aux événements de ce flux.
  8. Cliquez sur Suivant.
  9. Vérifiez la configuration de votre nouveau flux sur l'écran Finaliser, puis cliquez sur Envoyer.

Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.