Coletar registros de contexto do Jamf Pro

Compatível com:

Este documento explica como ingerir registros de contexto do Jamf Pro (contexto do dispositivo e do usuário) no Google Security Operations usando o AWS S3 com uma programação do Lambda e do EventBridge.

Antes de começar

  • Instância do Google SecOps
  • Acesso privilegiado ao locatário do Jamf Pro
  • Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge)

Configurar a função da API Jamf

  1. Faça login na UI da Web do Jamf.
  2. Acesse Configurações > seção "Sistema" > Funções e clientes da API.
  3. Selecione a guia Funções da API.
  4. Clique em Novo.
  5. Insira um nome de exibição para a função da API (por exemplo, context_role).
  6. Em Privilégios de função da API Jamf Pro, digite o nome de um privilégio e selecione-o no menu.

    • Inventário de computadores
    • Inventário de dispositivos móveis
  7. Clique em Salvar.

Configurar o cliente da API Jamf

  1. No Jamf Pro, acesse Configurações > seção "Sistema" > Funções e clientes da API.
  2. Selecione a guia Clientes da API.
  3. Clique em Novo.
  4. Insira um nome de exibição para o cliente da API (por exemplo, context_client).
  5. No campo Funções da API, adicione a função context_role que você criou anteriormente.
  6. Em Tempo de vida do token de acesso, insira o tempo em segundos para que os tokens de acesso sejam válidos.
  7. Clique em Salvar.
  8. Clique em Editar.
  9. Clique em Ativar cliente de API.
  10. Clique em Salvar.

Configurar a chave secreta do cliente Jamf

  1. No Jamf Pro, acesse o cliente de API recém-criado.
  2. Clique em Gerar chave secreta do cliente.
  3. Na tela de confirmação, clique em Criar secret.
  4. Salve os seguintes parâmetros em um local seguro:
    • URL de base: https://<your>.jamfcloud.com
    • ID do cliente: UUID.
    • Chave secreta do cliente: o valor é mostrado uma vez.

Configurar o bucket do AWS S3 e o IAM para o Google SecOps

  1. Crie um bucket do Amazon S3 seguindo este guia do usuário: Como criar um bucket.
  2. Salve o Nome e a Região do bucket para referência futura (por exemplo, jamfpro).
  3. Crie um usuário seguindo este guia: Como criar um usuário do IAM.
  4. Selecione o usuário criado.
  5. Selecione a guia Credenciais de segurança.
  6. Clique em Criar chave de acesso na seção Chaves de acesso.
  7. Selecione Serviço de terceiros como Caso de uso.
  8. Clique em Próxima.
  9. Opcional: adicione uma tag de descrição.
  10. Clique em Criar chave de acesso.
  11. Clique em Fazer o download do arquivo CSV para salvar a chave de acesso e a chave de acesso secreta para referência futura.
  12. Clique em Concluído.
  13. Selecione a guia Permissões.
  14. Clique em Adicionar permissões na seção Políticas de permissões.
  15. Selecione Adicionar permissões.
  16. Selecione Anexar políticas diretamente.
  17. Pesquise e selecione a política AmazonS3FullAccess.
  18. Clique em Próxima.
  19. Clique em Adicionar permissões

Configurar a política e o papel do IAM para uploads do S3

  1. JSON da política (substitua jamfpro se você tiver inserido um nome de bucket diferente):

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowPutJamfObjects",
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::jamfpro/*"
        }
      ]
    }
    
  2. Acesse Console da AWS > IAM > Políticas > Criar política > guia JSON.

  3. Copie e cole a política.

  4. Clique em Próxima > Criar política.

  5. Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.

  6. Anexe a política recém-criada.

  7. Nomeie a função como WriteJamfToS3Role e clique em Criar função.

Criar a função Lambda

  1. No console da AWS, acesse Lambda > Functions > Create function.
  2. Clique em Criar do zero.
  3. Informe os seguintes detalhes de configuração:
Configuração Valor
Nome jamf_pro_to_s3
Ambiente de execução Python 3.13
Arquitetura x86_64
Permissões WriteJamfToS3Role
  1. Depois que a função for criada, abra a guia Código, exclua o stub e insira o seguinte código (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. Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.

  3. Insira as seguintes variáveis de ambiente, substituindo pelos seus valores.

    Variáveis de ambiente

    Chave Exemplo
    S3_BUCKET jamfpro
    S3_PREFIX jamf-pro/context/
    AWS_REGION Selecione sua região
    JAMF_CLIENT_ID Insira o ID do cliente da Jamf
    JAMF_CLIENT_SECRET Insira a chave secreta do cliente da Jamf
    JAMF_BASE_URL Insira o URL do Jamf e substitua <your> em https://<your>.jamfcloud.com
    PAGE_SIZE 200
  4. Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua-função.

  5. Selecione a guia Configuração.

  6. No painel Configuração geral, clique em Editar.

  7. Mude Tempo limite para 5 minutos (300 segundos) e clique em Salvar.

Criar uma programação do EventBridge

  1. Acesse Amazon EventBridge > Scheduler > Criar programação.
  2. Informe os seguintes detalhes de configuração:
    • Programação recorrente: taxa (1 hour).
    • Destino: sua função Lambda.
    • Nome: jamfpro-context-schedule-1h.
  3. Clique em Criar programação.

Configurar um feed no Google SecOps para ingerir registros de contexto do Jamf Pro

  1. Acesse Configurações do SIEM > Feeds.
  2. Clique em + Adicionar novo feed.
  3. No campo Nome do feed, insira um nome para o feed (por exemplo, Jamf Pro Context logs).
  4. Selecione Amazon S3 V2 como o Tipo de origem.
  5. Selecione Contexto do Jamf Pro como o Tipo de registro.
  6. Clique em Próxima.
  7. Especifique valores para os seguintes parâmetros de entrada:
    • URI do S3: o URI do bucket
      • s3://jamfpro/jamf-pro/context/
        • Substitua jamfpro pelo nome real do bucket.
    • Opções de exclusão de fontes: selecione a opção de exclusão de acordo com sua preferência.
    • Idade máxima do arquivo: inclui arquivos modificados no último número de dias. O padrão é 180 dias.
    • ID da chave de acesso: chave de acesso do usuário com acesso ao bucket do S3.
    • Chave de acesso secreta: chave secreta do usuário com acesso ao bucket do S3.
    • Namespace do recurso: o namespace do recurso.
    • Rótulos de ingestão: o rótulo a ser aplicado aos eventos deste feed.
  8. Clique em Próxima.
  9. Revise a nova configuração do feed na tela Finalizar e clique em Enviar.

Precisa de mais ajuda? Receba respostas de membros da comunidade e profissionais do Google SecOps.