Recolha registos de contexto de entidades do Duo
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
- Inicie sessão no painel de administração do Duo.
- Aceda a Aplicações > Catálogo de aplicações.
- Adicione a aplicação API Admin.
- Registe os seguintes valores:
- Chave de integração (ikey)
- Chave secreta (skey)
- Nome de anfitrião da API (por exemplo,
api-XXXXXXXX.duosecurity.com
)
- Em Autorizações, ative a opção Conceder recurso – Leitura (para ler utilizadores, grupos, dispositivos/pontos finais).
- Guarde a aplicação.
Configure o contentor do AWS S3 e o IAM para o Google SecOps
- Crie um contentor do Amazon S3 seguindo este manual do utilizador: Criar um contentor
- Guarde o nome e a região do contentor para referência futura (por exemplo,
duo-context
). - Crie um utilizador seguindo este guia do utilizador: Criar um utilizador do IAM.
- Selecione o utilizador criado.
- Selecione o separador Credenciais de segurança.
- Clique em Criar chave de acesso na secção Chaves de acesso.
- Selecione Serviço de terceiros como o Exemplo de utilização.
- Clicar em Seguinte.
- Opcional: adicione uma etiqueta de descrição.
- Clique em Criar chave de acesso.
- Clique em Transferir ficheiro CSV para guardar a chave de acesso e a chave de acesso secreta para utilização posterior.
- Clique em Concluído.
- Selecione o separador Autorizações.
- Clique em Adicionar autorizações na secção Políticas de autorizações.
- Selecione Adicionar autorizações.
- Selecione Anexar políticas diretamente
- Pesquise e selecione a política AmazonS3FullAccess.
- Clicar em Seguinte.
- Clique em Adicionar autorizações.
Configure a política e a função de IAM para carregamentos do S3
- Aceda a AWS console > IAM > Policies > Create policy > separador JSON.
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:
- Substitua
Clique em Seguinte > Criar política.
Aceda a IAM > Funções > Criar função > Serviço AWS > Lambda.
Anexe a política criada recentemente.
Dê o nome
WriteDuoToS3Role
à função e clique em Criar função.
Crie a função Lambda
- Na consola da AWS, aceda a Lambda > Functions > Create function.
- Clique em Criar do zero.
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
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())
Aceda a Configuração > Variáveis de ambiente > Editar > Adicionar nova variável de ambiente.
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
Depois de criar a função, permaneça na respetiva página (ou abra Lambda > Functions > your-function).
Selecione o separador Configuração.
No painel Configuração geral, clique em Editar.
Altere Tempo limite para 5 minutos (300 segundos) e clique em Guardar.
Crie um horário do EventBridge
- Aceda a Amazon EventBridge > Scheduler > Create schedule.
- Indique os seguintes detalhes de configuração:
- Agenda recorrente: Taxa (
1 hour
). - Alvo: a sua função Lambda.
- Nome:
duo-entity-context-1h
.
- Agenda recorrente: Taxa (
- Clique em Criar programação.
Opcional: crie um utilizador e chaves da IAM só de leitura para o Google SecOps
- Na consola da AWS, aceda a IAM > Users e, de seguida, clique em Add users.
- 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.
- Utilizador: introduza um nome único (por exemplo,
- 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 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>" } ] }
Defina o nome como
secops-reader-policy
.Aceda a Criar política > pesquise/selecione > Seguinte > Adicionar autorizações.
Aceda a Credenciais de segurança > Chaves de acesso > Criar chave de acesso.
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
- Aceda a Definições do SIEM > Feeds.
- Clique em + Adicionar novo feed.
- No campo Nome do feed, introduza 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 Tipo de registo.
- Clicar em Seguinte.
- 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.
- URI do S3:
- Clicar em Seguinte.
- 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. |
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.