Coletar registros de contexto da entidade do Duo
Este documento explica como ingerir dados de contexto de entidade do Duo no Google Security Operations usando o Amazon S3. O analisador transforma os registros JSON em um modelo de dados unificado (UDM) extraindo primeiro os campos do JSON bruto e mapeando esses campos para atributos do UDM. Ele processa vários cenários de dados, incluindo informações de usuários e recursos, detalhes de software e rótulos de segurança, garantindo uma representação abrangente no esquema da UDM.
Antes de começar
- Instância do Google SecOps
- Acesso privilegiado ao locatário do Duo (aplicativo da API Admin)
- Acesso privilegiado à AWS (S3, IAM, Lambda, EventBridge)
Configurar o aplicativo da API Admin do Duo
- Faça login no painel de administração do Duo.
- Acesse Aplicativos > Catálogo de aplicativos.
- Adicione o aplicativo API Admin.
- Anote os seguintes valores:
- Chave de integração (ikey)
- Chave secreta (skey)
- Nome do host da API (por exemplo,
api-XXXXXXXX.duosecurity.com
)
- Em Permissões, ative Conceder recurso – leitura (para ler usuários, grupos, dispositivos/endpoints).
- Salve o aplicativo.
Configurar o bucket do AWS S3 e o IAM para o Google SecOps
- Crie um bucket do Amazon S3 seguindo este guia do usuário: Como criar um bucket
- Salve o Nome e a Região do bucket para referência futura (por exemplo,
duo-context
). - Crie um usuário seguindo este guia: Como criar um usuário do IAM.
- Selecione o usuário criado.
- Selecione a guia Credenciais de segurança.
- Clique em Criar chave de acesso na seção Chaves de acesso.
- Selecione Serviço de terceiros como o Caso de uso.
- Clique em Próxima.
- Opcional: adicione uma tag de descrição.
- Clique em Criar chave de acesso.
- Clique em Fazer o download do arquivo CSV para salvar a chave de acesso e a chave de acesso secreta para uso posterior.
- Clique em Concluído.
- Selecione a guia Permissões.
- Clique em Adicionar permissões na seção Políticas de permissões.
- Selecione Adicionar permissões.
- Selecione Anexar políticas diretamente.
- Pesquise e selecione a política AmazonS3FullAccess.
- Clique em Próxima.
- Clique em Adicionar permissões
Configurar a política e o papel do IAM para uploads do S3
- Acesse Console da AWS > IAM > Políticas > Criar política > guia JSON.
Insira 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 você inseriu um nome de bucket diferente:
- Substitua
Clique em Próxima > Criar política.
Acesse IAM > Funções > Criar função > Serviço da AWS > Lambda.
Anexe a política recém-criada.
Nomeie a função como
WriteDuoToS3Role
e clique em Criar função.
Criar a função Lambda
- No console da AWS, acesse Lambda > Functions > Create function.
- Clique em Criar do zero.
Informe os seguintes detalhes de configuração:
Configuração Valor Nome duo_entity_context_to_s3
Ambiente de execução Python 3.13 Arquitetura x86_64 Função de execução WriteDuoToS3Role
Depois que a função for criada, abra a guia Código, exclua o stub e insira 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())
Acesse Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
Insira as seguintes variáveis de ambiente, substituindo 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
Depois que a função for criada, permaneça na página dela ou abra Lambda > Functions > sua-função.
Selecione a guia Configuração.
No painel Configuração geral, clique em Editar.
Mude Tempo limite para 5 minutos (300 segundos) e clique em Salvar.
Criar uma programação do EventBridge
- Acesse Amazon EventBridge > Scheduler > Criar programação.
- Informe os seguintes detalhes de configuração:
- Programação recorrente: Taxa (
1 hour
). - Destino: sua função Lambda.
- Nome:
duo-entity-context-1h
.
- Programação recorrente: Taxa (
- Clique em Criar programação.
Opcional: criar um usuário e chaves do IAM somente leitura para o Google SecOps
- No console da AWS, acesse IAM > Usuários e clique em Adicionar usuários.
- Informe os seguintes detalhes de configuração:
- Usuário: insira um nome exclusivo (por exemplo,
secops-reader
) - Tipo de acesso: selecione Chave de acesso - Acesso programático.
- Clique em Criar usuário.
- Usuário: insira um nome exclusivo (por exemplo,
- Anexe a política de leitura mínima (personalizada): Usuários > selecione
secops-reader
> Permissões > Adicionar permissões > Anexar políticas diretamente > Criar política No editor JSON, insira 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>" } ] }
Defina o nome como
secops-reader-policy
.Acesse Criar política > pesquise/selecione > Próxima > Adicionar permissões.
Acesse Credenciais de segurança > Chaves de acesso > Criar chave de acesso.
Faça o download do CSV (esses valores são inseridos no feed).
Configurar um feed no Google SecOps para ingerir dados de contexto da entidade do Duo
- Acesse Configurações do SIEM > Feeds.
- Clique em + Adicionar novo feed.
- No campo Nome do feed, insira um nome para o feed (por exemplo,
Duo Entity Context
). - Selecione Amazon S3 V2 como o Tipo de origem.
- Selecione Dados de contexto da entidade do Duo como o Tipo de registro.
- Clique em Próxima.
- Especifique valores para os seguintes parâmetros de entrada:
- URI do S3:
s3://duo-context/duo/context/
- 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: padrão de 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 aplicado aos eventos deste feed.
- URI do S3:
- Clique em Próxima.
- Revise a nova configuração do feed na tela Finalizar e clique em Enviar.
Tabela de mapeamento da UDM
Campo de registro | Mapeamento do UDM | Lógica |
---|---|---|
ativado | entity.asset.deployment_status | Se "activated" for falso, defina como "DECOMISSIONED". Caso contrário, defina como "ACTIVE". |
browsers.browser_family | entity.asset.software.name | Extraído da matriz "browsers" no registro bruto. |
browsers.browser_version | entity.asset.software.version | Extraído da matriz "browsers" no registro bruto. |
device_name | entity.asset.hostname | Mapeado diretamente do registro bruto. |
disk_encryption_status | entity.asset.attribute.labels.key: "disk_encryption_status", entity.asset.attribute.labels.value: |
Mapeado diretamente do registro bruto e convertido em letras minúsculas. |
entity.user.email_addresses | Mapeado diretamente do registro bruto se ele contiver "@". Caso contrário, usa "username" ou "username1" se eles contiverem "@". | |
criptografado | entity.asset.attribute.labels.key: "Encrypted", entity.asset.attribute.labels.value: |
Mapeado diretamente do registro bruto e 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 do registro bruto e convertido em letras minúsculas. |
firewall_status | entity.asset.attribute.labels.key: "firewall_status", entity.asset.attribute.labels.value: |
Mapeado diretamente do registro bruto e 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 um carimbo de data/hora ISO8601 e mapeado. |
modelo | entity.asset.hardware.model | Mapeado diretamente do registro bruto. |
número | entity.user.phone_numbers | Mapeado diretamente do registro bruto. |
os_family | entity.asset.platform_software.platform | Mapeado para "WINDOWS", "LINUX" ou "MAC" com base no valor, sem diferenciação de maiúsculas e minúsculas. |
os_version | entity.asset.platform_software.platform_version | Mapeado diretamente do registro bruto. |
password_status | entity.asset.attribute.labels.key: "password_status", entity.asset.attribute.labels.value: |
Mapeado diretamente do registro bruto e 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 registro bruto. |
security_agents.version | entity.asset.software.version | Extraído da matriz "security_agents" no registro bruto. |
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 do registro bruto e convertido em letras minúsculas. |
tipo | entity.asset.type | Se o "type" do registro bruto contiver "mobile" (sem diferenciar maiúsculas de minúsculas), defina como "MOBILE". Caso contrário, defina como "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 usuário na matriz "users" e contiver "@". |
users.username | entity.user.userid | Nome de usuário extraído antes de "@" e usado como "userid" se for o primeiro usuário na matriz "users". |
entity.metadata.vendor_name | "Duo" | |
entity.metadata.product_name | "Dados de contexto da entidade do Duo" | |
entity.metadata.entity_type | RECURSO | |
entity.relations.entity_type | USUÁRIO | |
entity.relations.relationship | OWNS |
Precisa de mais ajuda? Receba respostas de membros da comunidade e profissionais do Google SecOps.