Recolha registos de contexto de entidades do Duo

Compatível com:

Este documento explica como introduzir dados de contexto de entidades do Duo no Google Security Operations através do Amazon S3. O analisador transforma os registos JSON num modelo de dados unificado (UDM) extraindo primeiro os campos do JSON não processado e, em seguida, mapeando esses campos para atributos do UDM. Processa vários cenários de dados, incluindo informações de utilizadores e recursos, detalhes de software e etiquetas de segurança, garantindo uma representação abrangente no esquema do UDM.

Antes de começar

  • Instância do Google SecOps
  • Acesso privilegiado ao inquilino do Duo (aplicação da API Admin)
  • Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge)

Configure a aplicação da API Duo Admin

  1. Inicie sessão no painel de administração do Duo.
  2. Aceda a Aplicações > Catálogo de aplicações.
  3. Adicione a aplicação API Admin.
  4. Registe os seguintes valores:
    • Chave de integração (ikey)
    • Chave secreta (skey)
    • Nome de anfitrião da API (por exemplo, api-XXXXXXXX.duosecurity.com)
  5. Em Autorizações, ative a opção Conceder recurso – Leitura (para ler utilizadores, grupos, dispositivos/pontos finais).
  6. Guarde a aplicação.

Configure o contentor do AWS S3 e o IAM para o Google SecOps

  1. Crie um contentor do Amazon S3 seguindo este manual do utilizador: Criar um contentor
  2. Guarde o nome e a região do contentor para referência futura (por exemplo, duo-context).
  3. Crie um utilizador seguindo este guia do utilizador: Criar um utilizador do IAM.
  4. Selecione o utilizador criado.
  5. Selecione o separador Credenciais de segurança.
  6. Clique em Criar chave de acesso na secção Chaves de acesso.
  7. Selecione Serviço de terceiros como o Exemplo de utilização.
  8. Clicar em Seguinte.
  9. Opcional: adicione uma etiqueta de descrição.
  10. Clique em Criar chave de acesso.
  11. Clique em Transferir ficheiro CSV para guardar a chave de acesso e a chave de acesso secreta para utilização posterior.
  12. Clique em Concluído.
  13. Selecione o separador Autorizações.
  14. Clique em Adicionar autorizações na secção Políticas de autorizações.
  15. Selecione Adicionar autorizações.
  16. Selecione Anexar políticas diretamente
  17. Pesquise e selecione a política AmazonS3FullAccess.
  18. Clicar em Seguinte.
  19. Clique em Adicionar autorizações.

Configure a política e a função de IAM para carregamentos do S3

  1. Aceda a AWS console > IAM > Policies > Create policy > separador JSON.
  2. Introduza a seguinte política:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowPutDuoObjects",
          "Effect": "Allow",
          "Action": "s3:PutObject",
          "Resource": "arn:aws:s3:::duo-context/*"
        }
      ]
    }
    
    • Substitua duo-context se tiver introduzido um nome de contentor diferente:
  3. Clique em Seguinte > Criar política.

  4. Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.

  5. Anexe a política criada recentemente.

  6. Dê o nome WriteDuoToS3Role à função e clique em Criar função.

Crie a função Lambda

  1. Na consola da AWS, aceda a Lambda > Functions > Create function.
  2. Clique em Criar do zero.
  3. Faculte os seguintes detalhes de configuração:

    Definição Valor
    Nome duo_entity_context_to_s3
    Runtime Python 3.13
    Arquitetura x86_64
    Função de execução WriteDuoToS3Role
  4. Depois de criar a função, abra o separador Código, elimine o fragmento e introduza o seguinte código (duo_entity_context_to_s3.py):

    #!/usr/bin/env python3
    
    import os, json, time, hmac, hashlib, base64, email.utils, urllib.parse
    from urllib.request import Request, urlopen
    import boto3
    
    # Env
    DUO_IKEY = os.environ["DUO_IKEY"]
    DUO_SKEY = os.environ["DUO_SKEY"]
    DUO_API_HOSTNAME = os.environ["DUO_API_HOSTNAME"].strip()
    S3_BUCKET = os.environ["S3_BUCKET"]
    S3_PREFIX = os.environ.get("S3_PREFIX", "duo/context/")
    # Default set can be adjusted via ENV
    RESOURCES = [r.strip() for r in os.environ.get(
        "RESOURCES",
        "users,groups,phones,endpoints,tokens,webauthncredentials,desktop_authenticators"
    ).split(",") if r.strip()]
    # Duo paging: default 100; max 500 for these endpoints
    LIMIT = int(os.environ.get("LIMIT", "500"))
    
    s3 = boto3.client("s3")
    
    def _canon_params(params: dict) -> str:
        """RFC3986 encoding with '~' unescaped, keys sorted lexicographically."""
        if not params:
            return ""
        parts = []
        for k in sorted(params.keys()):
            v = params[k]
            if v is None:
                continue
            ks = urllib.parse.quote(str(k), safe="~")
            vs = urllib.parse.quote(str(v), safe="~")
            parts.append(f"{ks}={vs}")
        return "&".join(parts)
    
    def _sign(method: str, host: str, path: str, params: dict) -> dict:
        """Construct Duo Admin API Authorization + Date headers (HMAC-SHA1)."""
        now = email.utils.formatdate()
        canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
        sig = hmac.new(DUO_SKEY.encode("utf-8"), canon.encode("utf-8"), hashlib.sha1).hexdigest()
        auth = base64.b64encode(f"{DUO_IKEY}:{sig}".encode("utf-8")).decode("utf-8")
        return {"Date": now, "Authorization": f"Basic {auth}"}
    
    def _call(method: str, path: str, params: dict) -> dict:
        host = DUO_API_HOSTNAME
        assert host.startswith("api-") and host.endswith(".duosecurity.com"), \
            "DUO_API_HOSTNAME must be e.g. api-XXXXXXXX.duosecurity.com"
        qs = _canon_params(params)
        url = f"https://{host}{path}" + (f"?{qs}" if method.upper() == "GET" and qs else "")
        req = Request(url, method=method.upper())
        for k, v in _sign(method, host, path, params).items():
            req.add_header(k, v)
        with urlopen(req, timeout=60) as r:
            return json.loads(r.read().decode("utf-8"))
    
    def _write_json(obj: dict, when: float, resource: str, page: int) -> str:
        prefix = S3_PREFIX.strip("/") + "/" if S3_PREFIX else ""
        key = f"{prefix}{time.strftime('%Y/%m/%d', time.gmtime(when))}/duo-{resource}-{page:05d}.json"
        s3.put_object(Bucket=S3_BUCKET, Key=key, Body=json.dumps(obj, separators=(",", ":")).encode("utf-8"))
        return key
    
    def _fetch_resource(resource: str) -> dict:
        """Fetch all pages for a list endpoint using limit/offset + metadata.next_offset."""
        path = f"/admin/v1/{resource}"
        offset = 0
        page = 0
        now = time.time()
        total_items = 0
    
        while True:
            params = {"limit": LIMIT, "offset": offset}
            data = _call("GET", path, params)
            _write_json(data, now, resource, page)
            page += 1
    
            resp = data.get("response")
            # most endpoints return a list; if not a list, count as 1 object page
            if isinstance(resp, list):
                total_items += len(resp)
            elif resp is not None:
                total_items += 1
    
            meta = data.get("metadata") or {}
            next_offset = meta.get("next_offset")
            if next_offset is None:
                break
    
            # Duo returns next_offset as int
            try:
                offset = int(next_offset)
            except Exception:
                break
    
        return {"resource": resource, "pages": page, "objects": total_items}
    
    def lambda_handler(event=None, context=None):
        results = []
        for res in RESOURCES:
            results.append(_fetch_resource(res))
        return {"ok": True, "results": results}
    
    if __name__ == "__main__":
        print(lambda_handler())
    
    
  5. Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.

  6. Introduza as seguintes variáveis de ambiente, substituindo-as pelos seus valores.

    Chave Exemplo
    S3_BUCKET duo-context
    S3_PREFIX duo/context/
    DUO_IKEY DIXYZ...
    DUO_SKEY ****************
    DUO_API_HOSTNAME api-XXXXXXXX.duosecurity.com
    LIMIT 200
    RESOURCES users,groups,phones,endpoints,tokens,webauthncredentials
  7. Depois de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your-function).

  8. Selecione o separador Configuração.

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

  10. Altere Tempo limite para 5 minutos (300 segundos) e clique em Guardar.

Crie um horário do EventBridge

  1. Aceda a Amazon EventBridge > Scheduler > Create schedule.
  2. Indique os seguintes detalhes de configuração:
    • Agenda recorrente: Taxa (1 hour).
    • Alvo: a sua função Lambda.
    • Nome: duo-entity-context-1h.
  3. Clique em Criar programação.

Opcional: crie um utilizador e chaves da IAM só de leitura para o Google SecOps

  1. Na consola da AWS, aceda a IAM > Users e, de seguida, clique em Add users.
  2. Indique os seguintes detalhes de configuração:
    • Utilizador: introduza um nome único (por exemplo, secops-reader)
    • Tipo de acesso: selecione Chave de acesso – Acesso programático
    • Clique em Criar utilizador.
  3. Anexe a política de leitura mínima (personalizada): Utilizadores > selecione secops-reader > Autorizações > Adicionar autorizações > Anexar políticas diretamente > Criar política
  4. No editor JSON, introduza a seguinte política:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["s3:GetObject"],
          "Resource": "arn:aws:s3:::<your-bucket>/*"
        },
        {
          "Effect": "Allow",
          "Action": ["s3:ListBucket"],
          "Resource": "arn:aws:s3:::<your-bucket>"
        }
      ]
    }
    
  5. Defina o nome como secops-reader-policy.

  6. Aceda a Criar política > pesquise/selecione > Seguinte > Adicionar autorizações.

  7. Aceda a Credenciais de segurança > Chaves de acesso > Criar chave de acesso.

  8. Transfira o CSV (estes valores são introduzidos no feed).

Configure um feed no Google SecOps para carregar dados de contexto de entidades do Duo

  1. Aceda a Definições do SIEM > Feeds.
  2. Clique em + Adicionar novo feed.
  3. No campo Nome do feed, introduza um nome para o feed (por exemplo, Duo Entity Context).
  4. Selecione Amazon S3 V2 como o Tipo de origem.
  5. Selecione Dados de contexto da entidade do Duo como Tipo de registo.
  6. Clicar em Seguinte.
  7. Especifique valores para os seguintes parâmetros de entrada:
    • URI do S3: s3://duo-context/duo/context/
    • Opções de eliminação de origens: selecione a opção de eliminação de acordo com a sua preferência.
    • Idade máxima do ficheiro: predefinição de 180 dias.
    • ID da chave de acesso: chave de acesso do utilizador com acesso ao contentor do S3.
    • Chave de acesso secreta: chave secreta do utilizador com acesso ao contentor do S3.
    • Espaço de nomes do recurso: o espaço de nomes do recurso.
    • Etiquetas de carregamento: a etiqueta aplicada aos eventos deste feed.
  8. Clicar em Seguinte.
  9. Reveja a nova configuração do feed no ecrã Finalizar e, de seguida, clique em Enviar.

Tabela de mapeamento da UDM

Campo de registo Mapeamento de UDM Lógica
ativado entity.asset.deployment_status Se "activated" for falso, é definido como "DECOMISSIONED", caso contrário, "ACTIVE".
browsers.browser_family entity.asset.software.name Extraído da matriz "browsers" no registo não processado.
browsers.browser_version entity.asset.software.version Extraído da matriz "browsers" no registo não processado.
device_name entity.asset.hostname Mapeado diretamente a partir do registo não processado.
disk_encryption_status entity.asset.attribute.labels.key: "disk_encryption_status", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
email entity.user.email_addresses Mapeado diretamente a partir do registo não processado se contiver "@". Caso contrário, usa "username" ou "username1" se contiverem "@".
encriptado entity.asset.attribute.labels.key: "Encrypted", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
epkey entity.asset.product_object_id Usado como "product_object_id" se estiver presente. Caso contrário, usa "phone_id" ou "token_id".
impressão digital entity.asset.attribute.labels.key: "Finger Print", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
firewall_status entity.asset.attribute.labels.key: "firewall_status", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
hardware_uuid entity.asset.asset_id Usado como "asset_id" se estiver presente. Caso contrário, usa "user_id".
last_seen entity.asset.last_discover_time Analisado como uma indicação de tempo ISO8601 e mapeado.
modelo entity.asset.hardware.model Mapeado diretamente a partir do registo não processado.
número entity.user.phone_numbers Mapeado diretamente a partir do registo não processado.
os_family entity.asset.platform_software.platform Mapeado para "WINDOWS", "LINUX" ou "MAC" com base no valor, sem distinção entre maiúsculas e minúsculas.
os_version entity.asset.platform_software.platform_version Mapeado diretamente a partir do registo não processado.
password_status entity.asset.attribute.labels.key: "password_status", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
phone_id entity.asset.product_object_id Usado como "product_object_id" se "epkey" não estiver presente. Caso contrário, usa "token_id".
security_agents.security_agent entity.asset.software.name Extraído da matriz "security_agents" no registo não processado.
security_agents.version entity.asset.software.version Extraído da matriz "security_agents" no registo não processado.
timestamp entity.metadata.collected_timestamp Preenche o campo "collected_timestamp" no objeto "metadata".
token_id entity.asset.product_object_id Usado como "product_object_id" se "epkey" e "phone_id" não estiverem presentes.
trusted_endpoint entity.asset.attribute.labels.key: "trusted_endpoint", entity.asset.attribute.labels.value: Mapeado diretamente a partir do registo não processado, convertido em letras minúsculas.
escrever entity.asset.type Se o "tipo" do registo não processado contiver "mobile" (sem distinção entre maiúsculas e minúsculas), defina como "MOBILE", caso contrário, "LAPTOP".
user_id entity.asset.asset_id Usado como "asset_id" se "hardware_uuid" não estiver presente.
users.email entity.user.email_addresses Usado como "email_addresses" se for o primeiro utilizador na matriz "users" e contiver "@".
users.username entity.user.userid Nome de utilizador extraído antes de "@" e usado como "userid" se for o primeiro utilizador na matriz "users".
entity.metadata.vendor_name "Duo"
entity.metadata.product_name "Duo Entity Context Data"
entity.metadata.entity_type RECURSO
entity.relations.entity_type UTILIZADOR
entity.relations.relationship OWNS

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