Collecter les journaux de contexte d'entité Duo

Compatible avec :

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

  1. Connectez-vous au panneau d'administration Duo.
  2. Accédez à Applications > Catalogue d'applications.
  3. Ajoutez l'application API Admin.
  4. 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)
  5. Dans Autorisations, activez Accorder l'accès à la ressource : lecture (pour lire les utilisateurs, les groupes, les appareils/points de terminaison).
  6. Enregistrez l'application.

Configurer un bucket AWS S3 et IAM pour Google SecOps

  1. Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket.
  2. Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple, duo-context).
  3. Créez un utilisateur en suivant ce guide : Créer un utilisateur IAM.
  4. Sélectionnez l'utilisateur créé.
  5. Sélectionnez l'onglet Informations d'identification de sécurité.
  6. Cliquez sur Créer une clé d'accès dans la section Clés d'accès.
  7. Sélectionnez Service tiers comme Cas d'utilisation.
  8. Cliquez sur Suivant.
  9. Facultatif : ajoutez un tag de description.
  10. Cliquez sur Créer une clé d'accès.
  11. 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.
  12. Cliquez sur OK.
  13. Sélectionnez l'onglet Autorisations.
  14. Cliquez sur Ajouter des autorisations dans la section Règles d'autorisation.
  15. Sélectionnez Ajouter des autorisations.
  16. Sélectionnez Joindre directement des règles.
  17. Recherchez et sélectionnez la règle AmazonS3FullAccess.
  18. Cliquez sur Suivant.
  19. Cliquez sur Ajouter des autorisations.

Configurer la stratégie et le rôle IAM pour les importations S3

  1. Accédez à la console AWS> IAM> Policies> Create policy> onglet JSON.
  2. 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 :
  3. Cliquez sur Suivant > Créer une règle.

  4. Accédez à IAM > Rôles > Créer un rôle > Service AWS > Lambda.

  5. Associez la règle que vous venez de créer.

  6. Nommez le rôle WriteDuoToS3Role, puis cliquez sur Créer un rôle.

Créer la fonction Lambda

  1. Dans la console AWS, accédez à Lambda > Fonctions > Créer une fonction.
  2. Cliquez sur Créer à partir de zéro.
  3. 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
  4. 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())
    
    
  5. Accédez à Configuration> Variables d'environnement> Modifier> Ajouter une variable d'environnement.

  6. 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
  7. Une fois la fonction créée, restez sur sa page (ou ouvrez Lambda > Fonctions > votre-fonction).

  8. Accédez à l'onglet Configuration.

  9. Dans le panneau Configuration générale, cliquez sur Modifier.

  10. Définissez le délai avant expiration sur 5 minutes (300 secondes), puis cliquez sur Enregistrer.

Créer une programmation EventBridge

  1. Accédez à Amazon EventBridge> Scheduler> Create schedule (Créer une programmation).
  2. Fournissez les informations de configuration suivantes :
    • Planning récurrent : Tarif (1 hour).
    • Cible : votre fonction Lambda.
    • Nom : duo-entity-context-1h.
  3. Cliquez sur Créer la programmation.

Facultatif : Créez un utilisateur et des clés IAM en lecture seule pour Google SecOps

  1. Dans la console AWS, accédez à IAM> Utilisateurs, puis cliquez sur Ajouter des utilisateurs.
  2. 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.
  3. 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
  4. 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>"
        }
      ]
    }
    
  5. Définissez le nom sur secops-reader-policy.

  6. Accédez à Créer une règle > recherchez/sélectionnez > Suivant > Ajouter des autorisations.

  7. Accédez à Identifiants de sécurité > Clés d'accès > Créer une clé d'accès.

  8. 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

  1. Accédez à Paramètres SIEM> Flux.
  2. Cliquez sur + Ajouter un flux.
  3. Dans le champ Nom du flux, saisissez un nom pour le flux (par exemple, Duo Entity Context).
  4. Sélectionnez Amazon S3 V2 comme type de source.
  5. Sélectionnez Données contextuelles de l'entité Duo comme Type de journal.
  6. Cliquez sur Suivant.
  7. 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.
  8. Cliquez sur Suivant.
  9. 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.
e-mail 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.