Collecter les journaux de contexte d'entité Duo
Ce document explique comment ingérer des données de contexte d'entité Duo dans Google Security Operations à l'aide d'Amazon S3. L'analyseur transforme les journaux JSON en modèle de données unifié (UDM) en extrayant d'abord les champs du JSON brut, puis en mappant ces champs aux attributs UDM. Il gère différents scénarios de données, y compris les informations sur les utilisateurs et les assets, les détails des logiciels et les libellés de sécurité, ce qui garantit une représentation complète dans le schéma UDM.
Avant de commencer
- Instance Google SecOps
- Accès privilégié au locataire Duo (application de l'API Admin)
- Accès privilégié à AWS (S3, IAM, Lambda, EventBridge)
Configurer l'application Duo Admin API
- Connectez-vous au panneau d'administration Duo.
- Accédez à Applications > Catalogue d'applications.
- Ajoutez l'application API Admin.
- Notez les valeurs suivantes :
- Clé d'intégration (ikey)
- Clé secrète (skey)
- Nom d'hôte de l'API (par exemple,
api-XXXXXXXX.duosecurity.com
)
- Dans Autorisations, activez Accorder l'accès à la ressource : lecture (pour lire les utilisateurs, les groupes, les appareils/points de terminaison).
- Enregistrez l'application.
Configurer un bucket AWS S3 et IAM pour Google SecOps
- Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
- Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple,
duo-context
). - Créez un utilisateur en suivant ce guide : Créer un utilisateur IAM.
- Sélectionnez l'utilisateur créé.
- Sélectionnez l'onglet Informations d'identification de sécurité.
- Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
- Sélectionnez Service tiers comme Cas d'utilisation.
- Cliquez sur Suivant.
- Facultatif : ajoutez un tag de description.
- Cliquez sur Créer une clé d'accès.
- Cliquez sur Télécharger le fichier CSV pour enregistrer la clé d'accès et la clé d'accès secrète pour une utilisation ultérieure.
- Cliquez sur OK.
- Sélectionnez l'onglet Autorisations.
- Cliquez sur Ajouter des autorisations dans la section Règles d'autorisation.
- Sélectionnez Ajouter des autorisations.
- Sélectionnez Joindre directement des règles.
- Recherchez et sélectionnez la règle AmazonS3FullAccess.
- Cliquez sur Suivant.
- Cliquez sur Ajouter des autorisations.
Configurer la stratégie et le rôle IAM pour les importations S3
- Accédez à la console AWS> IAM> Policies> Create policy> onglet JSON.
Saisissez la règle suivante :
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutDuoObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::duo-context/*" } ] }
- Remplacez
duo-context
si vous avez saisi un autre nom de bucket :
- Remplacez
Cliquez sur Suivant > Créer une règle.
Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.
Associez la règle que vous venez de créer.
Nommez le rôle
WriteDuoToS3Role
, puis cliquez sur Créer un rôle.
Créer la fonction Lambda
- Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
- Cliquez sur Créer à partir de zéro.
Fournissez les informations de configuration suivantes :
Paramètre Valeur Nom duo_entity_context_to_s3
Durée d'exécution Python 3.13 Architecture x86_64 Rôle d'exécution WriteDuoToS3Role
Une fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et saisissez le code suivant (
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())
Accédez à Configuration> Variables d'environnement> Modifier> Ajouter une variable d'environnement.
Saisissez les variables d'environnement suivantes en remplaçant par vos valeurs.
Clé Exemple 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
Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre-fonction).
Accédez à l'onglet Configuration.
Dans le panneau Configuration générale, cliquez sur Modifier.
Définissez le délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.
Créer une programmation EventBridge
- Accédez à Amazon EventBridge> Scheduler> Create schedule (Créer une programmation).
- Fournissez les informations de configuration suivantes :
- Planning récurrent : Tarif (
1 hour
). - Cible : votre fonction Lambda.
- Nom :
duo-entity-context-1h
.
- Planning récurrent : Tarif (
- Cliquez sur Créer la programmation.
Facultatif : Créez un utilisateur et des clés IAM en lecture seule pour Google SecOps
- Dans la console AWS, accédez à IAM> Utilisateurs, puis cliquez sur Ajouter des utilisateurs.
- Fournissez les informations de configuration suivantes :
- Utilisateur : saisissez un nom unique (par exemple,
secops-reader
). - Type d'accès : sélectionnez Clé d'accès – Accès programmatique.
- Cliquez sur Créer un utilisateur.
- Utilisateur : saisissez un nom unique (par exemple,
- Associez une règle de lecture minimale (personnalisée) : Utilisateurs> sélectionnez
secops-reader
> Autorisations> Ajouter des autorisations> Associer des règles directement> Créer une règle Dans l'éditeur JSON, saisissez la stratégie suivante :
{ "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>" } ] }
Définissez le nom sur
secops-reader-policy
.Accédez à Créer une règle > recherchez/sélectionnez > Suivant > Ajouter des autorisations.
Accédez à Identifiants de sécurité > Clés d'accès > Créer une clé d'accès.
Téléchargez le CSV (ces valeurs sont saisies dans le flux).
Configurer un flux dans Google SecOps pour ingérer les données de contexte des entités Duo
- Accédez à Paramètres SIEM> Flux.
- Cliquez sur + Ajouter un flux.
- Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple,
Duo Entity Context
). - Sélectionnez Amazon S3 V2 comme type de source.
- Sélectionnez Données contextuelles de l'entité Duo comme Type de journal.
- Cliquez sur Suivant.
- Spécifiez les valeurs des paramètres d'entrée suivants :
- URI S3 :
s3://duo-context/duo/context/
- Options de suppression de la source : sélectionnez l'option de suppression de votre choix.
- Âge maximal des fichiers : 180 jours par défaut.
- ID de clé d'accès : clé d'accès utilisateur ayant accès au bucket S3.
- Clé d'accès secrète : clé secrète de l'utilisateur ayant accès au bucket S3.
- Espace de noms de l'élément : espace de noms de l'élément.
- Libellés d'ingestion : libellé appliqué aux événements de ce flux.
- URI S3 :
- Cliquez sur Suivant.
- Vérifiez la configuration de votre nouveau flux sur l'écran Finaliser, puis cliquez sur Envoyer.
Table de mappage UDM
Champ de journal | Mappage UDM | Logique |
---|---|---|
activé | entity.asset.deployment_status | Si la valeur de "activated" est "false", définissez la valeur sur "DECOMISSIONED", sinon sur "ACTIVE". |
browsers.browser_family | entity.asset.software.name | Extrait du tableau "browsers" (navigateurs) dans le journal brut. |
browsers.browser_version | entity.asset.software.version | Extrait du tableau "browsers" (navigateurs) dans le journal brut. |
device_name | entity.asset.hostname | Directement mappé à partir du journal brut. |
disk_encryption_status | entity.asset.attribute.labels.key: "disk_encryption_status", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
entity.user.email_addresses | Directement mappé à partir du journal brut s'il contient "@", sinon utilise "username" ou "username1" s'il contient "@". | |
chiffré | entity.asset.attribute.labels.key: "Encrypted", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
epkey | entity.asset.product_object_id | Utilisé comme "product_object_id" s'il est présent, sinon utilise "phone_id" ou "token_id". |
fingerprint | entity.asset.attribute.labels.key: "Finger Print", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
firewall_status | entity.asset.attribute.labels.key: "firewall_status", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
hardware_uuid | entity.asset.asset_id | Utilisé comme "asset_id" s'il est présent, sinon "user_id" est utilisé. |
last_seen | entity.asset.last_discover_time | Analysé en tant que code temporel ISO8601 et mappé. |
modèle | entity.asset.hardware.model | Directement mappé à partir du journal brut. |
Total | entity.user.phone_numbers | Directement mappé à partir du journal brut. |
os_family | entity.asset.platform_software.platform | Mappé sur "WINDOWS", "LINUX" ou "MAC" en fonction de la valeur (non sensible à la casse). |
os_version | entity.asset.platform_software.platform_version | Directement mappé à partir du journal brut. |
password_status | entity.asset.attribute.labels.key: "password_status", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
phone_id | entity.asset.product_object_id | Utilisé comme "product_object_id" si "epkey" n'est pas présent, sinon utilise "token_id". |
security_agents.security_agent | entity.asset.software.name | Extrait du tableau "security_agents" du journal brut. |
security_agents.version | entity.asset.software.version | Extrait du tableau "security_agents" du journal brut. |
timestamp | entity.metadata.collected_timestamp | Remplit le champ "collected_timestamp" dans l'objet "metadata". |
token_id | entity.asset.product_object_id | Utilisé comme "product_object_id" si "epkey" et "phone_id" ne sont pas présents. |
trusted_endpoint | entity.asset.attribute.labels.key: "trusted_endpoint", entity.asset.attribute.labels.value: |
Mappé directement à partir du journal brut, converti en minuscules. |
type | entity.asset.type | Si le "type" du journal brut contient "mobile" (sans tenir compte de la casse), définissez la valeur sur "MOBILE", sinon sur "LAPTOP". |
user_id | entity.asset.asset_id | Utilisé comme "asset_id" si "hardware_uuid" n'est pas présent. |
users.email | entity.user.email_addresses | Utilisé comme "email_addresses" s'il s'agit du premier utilisateur du tableau "users" et qu'il contient "@". |
users.username | entity.user.userid | Nom d'utilisateur extrait avant "@" et utilisé comme "userid" s'il s'agit du premier utilisateur du tableau "users". |
entity.metadata.vendor_name | "Duo" | |
entity.metadata.product_name | "Données contextuelles des entités Duo" | |
entity.metadata.entity_type | ASSET | |
entity.relations.entity_type | UTILISATEUR | |
entity.relations.relationship | OWNS |
Vous avez encore besoin d'aide ? Obtenez des réponses de membres de la communauté et de professionnels Google SecOps.