Coletar registros de contexto do Jamf Pro

Compatível com:

Este documento explica como ingerir registros de contexto do Jamf Pro (contexto de dispositivo e usuário) no Google Security Operations usando o Cloud Storage com funções do Cloud Run, Pub/Sub e Cloud Scheduler. O Jamf Pro é uma solução de gerenciamento abrangente para dispositivos Apple, que oferece inventário de dispositivos, contexto do usuário e recursos de gerenciamento de configuração.

Antes de começar

Verifique se você atende os seguintes pré-requisitos:

  • Uma instância do Google SecOps
  • Um projeto do Google Cloud com a API Storage Cloud ativada
  • Permissões para criar e gerenciar buckets do Cloud Storage
  • Permissões para gerenciar políticas do IAM em buckets do Cloud Storage
  • Permissões para criar serviços do Cloud Run, tópicos do Pub/Sub e jobs do Cloud Scheduler
  • Acesso privilegiado ao locatário do Jamf Pro

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 Ciclo 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 base: https://<your>.jamfcloud.com
    • ID do cliente: UUID.
    • Chave secreta do cliente: o valor é mostrado apenas uma vez.

criar o bucket do Cloud Storage

  1. Acesse o console doGoogle Cloud .
  2. Selecione ou crie um projeto.
  3. No menu de navegação, acesse Cloud Storage > Buckets.
  4. Clique em Criar bucket.
  5. Informe os seguintes detalhes de configuração:

    Configuração Valor
    Nomeie seu bucket Insira um nome globalmente exclusivo, por exemplo, jamfpro.
    Tipo de local Escolha de acordo com suas necessidades (região, birregional, multirregional)
    Local Selecione o local (por exemplo, us-central1).
    Classe de armazenamento Padrão (recomendado para registros acessados com frequência)
    Controle de acesso Uniforme (recomendado)
    Ferramentas de proteção Opcional: ativar o controle de versões de objetos ou a política de retenção
  6. Clique em Criar.

Criar uma conta de serviço para a função do Cloud Run

A função do Cloud Run precisa de uma conta de serviço com permissões para gravar no bucket do Cloud Storage e ser invocada pelo Pub/Sub.

Criar conta de serviço

  1. No console, acesse IAM e administrador > Contas de serviço.
  2. Clique em Criar conta de serviço.
  3. Informe os seguintes detalhes de configuração:
    • Nome da conta de serviço: insira jamf-pro-collector-sa.
    • Descrição da conta de serviço: insira Service account for Cloud Run function to collect Jamf Pro context logs.
  4. Clique em Criar e continuar.
  5. Na seção Conceder acesso a essa conta de serviço ao projeto, adicione os seguintes papéis:
    1. Clique em Selecionar papel.
    2. Pesquise e selecione Administrador de objetos do Storage.
    3. Clique em + Adicionar outro papel.
    4. Pesquise e selecione Invocador do Cloud Run.
    5. Clique em + Adicionar outro papel.
    6. Pesquise e selecione Invocador de funções do Cloud.
  6. Clique em Continuar.
  7. Clique em Concluído.

Essas funções são necessárias para:

  • Administrador de objetos do Storage: grava registros no bucket do Cloud Storage e gerencia arquivos de estado.
  • Invocador do Cloud Run: permite que o Pub/Sub invoque a função.
  • Invocador do Cloud Functions: permite a invocação de funções.

Conceder permissões do IAM no bucket do Cloud Storage

Conceda permissões de gravação à conta de serviço no bucket do Cloud Storage:

  1. Acesse Cloud Storage > Buckets.
  2. Clique no nome do bucket.
  3. Acesse a guia Permissões.
  4. Clique em Conceder acesso.
  5. Informe os seguintes detalhes de configuração:
    • Adicionar principais: insira o e-mail da conta de serviço (por exemplo, jamf-pro-collector-sa@PROJECT_ID.iam.gserviceaccount.com).
    • Atribuir papéis: selecione Administrador de objetos do Storage.
  6. Clique em Salvar.

Criar tópico Pub/Sub

Crie um tópico do Pub/Sub em que o Cloud Scheduler vai publicar e a função do Cloud Run vai se inscrever.

  1. No console, acesse Pub/Sub > Tópicos.
  2. Selecione Criar tópico.
  3. Informe os seguintes detalhes de configuração:
    • ID do tópico: insira jamf-pro-context-trigger.
    • Não mude as outras configurações.
  4. Clique em Criar.

Criar uma função do Cloud Run para coletar registros

A função do Cloud Run será acionada por mensagens do Pub/Sub do Cloud Scheduler para buscar registros da API Jamf Pro e gravá-los no Cloud Storage.

  1. No console, acesse o Cloud Run.
  2. Clique em Criar serviço.
  3. Selecione Função (use um editor in-line para criar uma função).
  4. Na seção Configurar, forneça os seguintes detalhes de configuração:

    Configuração Valor
    Nome do serviço jamf-pro-context-collector
    Região Selecione a região que corresponde ao seu bucket do Cloud Storage (por exemplo, us-central1).
    Ambiente de execução Selecione Python 3.12 ou uma versão mais recente.
  5. Na seção Gatilho (opcional):

    1. Clique em + Adicionar gatilho.
    2. Selecione Pub/Sub.
    3. Em Selecionar um tópico do Pub/Sub, escolha o tópico do Pub/Sub (jamf-pro-context-trigger).
    4. Clique em Salvar.
  6. Na seção Autenticação:

    1. Selecione Exigir autenticação.
    2. Confira o Identity and Access Management (IAM).
  7. Role a tela para baixo e abra Contêineres, rede, segurança.

  8. Acesse a guia Segurança:

    • Conta de serviço: selecione a conta de serviço (jamf-pro-collector-sa).
  9. Acesse a guia Contêineres:

    1. Clique em Variáveis e secrets.
    2. Clique em + Adicionar variável para cada variável de ambiente:
    Nome da variável Valor de exemplo
    GCS_BUCKET jamfpro
    GCS_PREFIX jamf-pro/context/
    JAMF_CLIENT_ID Insira o ID do cliente do 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
  10. Role a tela para baixo na guia Variáveis e secrets até Solicitações:

    • Tempo limite da solicitação: insira 600 segundos (10 minutos).
  11. Acesse a guia Configurações em Contêineres:

    • Na seção Recursos:
      • Memória: selecione 512 MiB ou mais.
      • CPU: selecione 1.
    • Clique em Concluído.
  12. Role a tela para baixo até Ambiente de execução:

    • Selecione Padrão (recomendado).
  13. Na seção Escalonamento de revisão:

    • Número mínimo de instâncias: insira 0.
    • Número máximo de instâncias: insira 100 ou ajuste com base na carga esperada.
  14. Clique em Criar.

  15. Aguarde a criação do serviço (1 a 2 minutos).

  16. Depois que o serviço for criado, o editor de código inline será aberto automaticamente.

Adicionar código da função

  1. Insira main em Ponto de entrada da função
  2. No editor de código inline, crie dois arquivos:

    • Primeiro arquivo: main.py::
    import functions_framework
    from google.cloud import storage
    import json
    import os
    import urllib3
    from datetime import datetime, timezone
    import gzip
    import io
    import time
    
    # Initialize HTTP client
    http = urllib3.PoolManager()
    
    # Initialize Storage client
    storage_client = storage.Client()
    
    # Configuration
    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")
    GCS_BUCKET = os.environ.get("GCS_BUCKET")
    GCS_PREFIX = os.environ.get("GCS_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"
    ]
    
    # Cached access token, refreshed shortly before it expires
    _token_cache = {"value": None, "expires_at": 0.0}
    
    def get_token():
        """OAuth2 client credentials > access_token"""
        url = f"{BASE_URL}/api/oauth/token"
    
        fields = {
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET
        }
    
        # encode_multipart=False is required. It sends a real
        # application/x-www-form-urlencoded body, which is what the Jamf token
        # endpoint expects. Without it, urllib3 sends a multipart/form-data body
        # and Jamf cannot read grant_type, client_id and client_secret.
        response = http.request(
            'POST',
            url,
            fields=fields,
            encode_multipart=False,
            timeout=30.0
        )
    
        if response.status != 200:
            raise Exception(f"Failed to get token: {response.status} {response.data.decode('utf-8')}")
    
        data = json.loads(response.data.decode('utf-8'))
        return data["access_token"], int(data.get("expires_in", 1200))
    
    def get_valid_token():
        """Return a cached token, refreshing it before it expires."""
        now = time.time()
        if _token_cache["value"] and now < _token_cache["expires_at"] - 60:
            return _token_cache["value"]
    
        token, ttl = get_token()
        _token_cache["value"] = token
        _token_cache["expires_at"] = now + ttl
        return token
    
    def fetch_page(token, page):
        """GET /api/v1/computers-inventory with sections & pagination"""
        url = f"{BASE_URL}/api/v1/computers-inventory"
    
        # Build query parameters
        params = [("page", str(page)), ("page-size", str(PAGE_SIZE))]
        params.extend([("section", s) for s in SECTIONS])
    
        # Encode parameters
        query_string = "&".join([f"{k}={v}" for k, v in params])
        full_url = f"{url}?{query_string}"
    
        headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/json"
        }
    
        response = http.request(
            'GET',
            full_url,
            headers=headers,
            timeout=60.0
        )
    
        if response.status != 200:
            raise Exception(f"Failed to fetch page {page}: {response.status} {response.data.decode('utf-8')}")
    
        return json.loads(response.data.decode('utf-8'))
    
    def write_ndjson_gz(objs, when):
        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 = GCS_PREFIX.strip("/") + "/" if GCS_PREFIX else ""
        key = f"{prefix}{when:%Y/%m/%d}/jamf_pro_context_{int(when.timestamp())}.ndjson.gz"
    
        bucket = storage_client.bucket(GCS_BUCKET)
        blob = bucket.blob(key)
        blob.upload_from_file(buf, content_type="application/gzip")
    
        return key
    
    @functions_framework.cloud_event
    def main(cloud_event):
        """
        Cloud Run function triggered by Pub/Sub to fetch Jamf Pro context logs and write to Cloud Storage.
    
        Each computer is written exactly as Jamf returns it: the inventory sections
        (general, hardware, operatingSystem, userAndLocation, and so on) are top-level
        keys on the record. The default parser reads this shape directly, so the records
        are not reshaped here.
    
        Args:
            cloud_event: CloudEvent object containing Pub/Sub message
        """
    
        if not all([BASE_URL, CLIENT_ID, CLIENT_SECRET, GCS_BUCKET]):
            print("Error: Missing required environment variables")
            return
    
        try:
            page = 0
            total = 0
            batch = []
            now = datetime.now(timezone.utc)
    
            while True:
                payload = fetch_page(get_valid_token(), page)
                results = payload.get("results") or []
    
                if not results:
                    break
    
                # Write the Jamf records as returned; do not rename or nest fields.
                batch.extend(results)
                total += len(results)
    
                if len(batch) >= 5000:
                    key = write_ndjson_gz(batch, now)
                    print(f"Wrote {len(batch)} records to gs://{GCS_BUCKET}/{key}")
                    batch = []
    
                if len(results) < PAGE_SIZE:
                    break
    
                page += 1
    
            if batch:
                key = write_ndjson_gz(batch, now)
                print(f"Wrote {len(batch)} records to gs://{GCS_BUCKET}/{key}")
    
            print(f"Successfully processed {total} total records")
    
        except Exception as e:
            print(f"Error processing Jamf Pro context logs: {str(e)}")
            raise
    

    • Segundo arquivo: requirements.txt:
    functions-framework==3.*
    google-cloud-storage==2.*
    urllib3>=2.0.0
    
  3. Clique em Implantar para salvar e implantar a função.

  4. Aguarde a conclusão da implantação (2 a 3 minutos).

Criar o job do Cloud Scheduler

O Cloud Scheduler vai publicar mensagens no tópico do Pub/Sub em intervalos regulares, acionando a função do Cloud Run.

  1. No console, acesse o Cloud Scheduler.
  2. Clique em Criar job.
  3. Informe os seguintes detalhes de configuração:

    Configuração Valor
    Nome jamfpro-context-schedule-1h
    Região Selecione a mesma região da função do Cloud Run
    Frequência 0 * * * * (de hora em hora)
    Fuso horário Selecionar fuso horário (UTC recomendado)
    Tipo de destino Pub/Sub
    Tópico Selecione o tópico do Pub/Sub (jamf-pro-context-trigger).
    Corpo da mensagem {} (objeto JSON vazio)
  4. Clique em Criar.

Testar o job do programador

  1. No console do Cloud Scheduler, encontre seu job.
  2. Clique em Forçar execução para acionar manualmente.
  3. Aguarde alguns segundos e acesse Cloud Run > Serviços > jamf-pro-context-collector > Registros.
  4. Verifique se a função foi executada com sucesso.
  5. Verifique o bucket do Cloud Storage para confirmar se os registros foram gravados.

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. Clique em Configurar um único feed.
  4. No campo Nome do feed, insira um nome para o feed (por exemplo, Jamf Pro Context logs).
  5. Selecione Google Cloud Storage V2 como o Tipo de origem.
  6. Selecione Contexto do Jamf Pro como o Tipo de registro.
  7. Clique em Receber conta de serviço. Um e-mail exclusivo da conta de serviço é exibido, por exemplo:

    chronicle-12345678@chronicle-gcp-prod.iam.gserviceaccount.com
    
  8. Copie o endereço de e-mail. Você vai usá-la na próxima etapa.

  9. Clique em Próxima.

  10. Especifique valores para os seguintes parâmetros de entrada:

  11. URL do bucket de armazenamento: insira o URI do bucket do Cloud Storage com o caminho do prefixo:

    gs://jamfpro/jamf-pro/context/
    

    Substitua jamfpro pelo nome real do bucket.

  • Opção de exclusão da fonte: selecione a opção de exclusão de acordo com sua preferência:

    • Nunca: nunca exclui arquivos após as transferências (recomendado para testes).
    • Excluir arquivos transferidos: exclui os arquivos após a transferência.
    • Excluir arquivos transferidos e diretórios vazios: exclui arquivos e diretórios vazios após a transferência.
  • Idade máxima do arquivo: inclui arquivos modificados nos últimos dias. O padrão é de 180 dias.

  • Namespace do recurso: o namespace do recurso.

  • Rótulos de ingestão: o rótulo a ser aplicado aos eventos deste feed.

  1. Clique em Próxima.
  2. Revise a nova configuração do feed na tela Finalizar e clique em Enviar.

Conceder permissões do IAM à conta de serviço do Google SecOps

A conta de serviço do Google SecOps precisa do papel Leitor de objetos do Storage no seu bucket do Cloud Storage.

  1. Acesse Cloud Storage > Buckets.
  2. Clique no nome do bucket.
  3. Acesse a guia Permissões.
  4. Clique em Conceder acesso.
  5. Informe os seguintes detalhes de configuração:
  6. Adicionar participantes: cole o e-mail da conta de serviço do Google SecOps.
  7. Atribuir papéis: selecione Leitor de objetos do Storage.
  8. Clique em Salvar.

Registro de alterações

Ver o registro de alterações deste analisador

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