From 010f45b8cf3c62159a0829c5ad1c7fa35590b446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Laporte?= Date: Wed, 28 Oct 2026 16:45:44 +0100 Subject: [PATCH] 1er upload --- README.md | 106 ++++++++++++++++- WAPT/description.txt | 1 + setup.py | 272 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 WAPT/description.txt create mode 100644 setup.py diff --git a/README.md b/README.md index 6d02f35..7d45be9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,103 @@ -# comi-audit-gpresult-user - -affiche le gpresult du user actif dans les données d'audit \ No newline at end of file +**form-gpresult-user** +Paquet WAPT d'audit des stratégies de groupe de l'utilisateur ayant une session active sur le poste. Il collecte les données GPResult et les expose sous forme structurée dans **`audit-gpresult.user-gpo`**. +**Identification dans le dépôt** +- **Nom du paquet :** `form-gpresult-user` +- **Libellé :** Audit GPO — Utilisateur actif +- **Description proposée pour le champ `description` de `WAPT/control` :** +``` +Audit des GPO de l'utilisateur actif : identification du compte, listes des GPO appliquees et non appliquees et compteurs dans audit-gpresult.user-gpo. +  +``` +Le fichier `WAPT/description.txt` contient cette ligne à reporter dans le fichier `WAPT/control` existant. Ce n'est pas un fichier de contrôle complet : conserver les autres métadonnées du paquet, notamment la version, les dépendances et les informations de signature. +**Fonctionnement** +1. L'installation appelle `install()`, volontairement vide. +2. L'audit recherche les sessions Windows actives avec les API WTS, via `win32ts`. +3. Il sélectionne l'unique compte actif et lance : +4. +``` +gpresult.exe /USER "DOMAINE\utilisateur" /SCOPE USER /X "" /F +  +``` +5. Il analyse la section XML `UserResults`, puis classe les GPO et calcule les compteurs. +6. Il transmet directement un dictionnaire à `WAPT.write_audit_data_if_changed`, sans convertir le résultat en chaîne JSON. +7. Le dossier et le rapport XML temporaires sont supprimés à la sortie du bloc de collecte, y compris en cas d'exception. +L'audit s'exécute dans le contexte de l'agent WAPT et cible explicitement les données RSoP de l'utilisateur. Il n'utilise pas `session_setup()`. Il consulte les données disponibles sans lancer de `gpupdate` ni modifier les stratégies du poste. +**Prérequis** +- Poste Windows disposant de `gpresult.exe`, accessible depuis le contexte de l'agent. +- Agent WAPT fournissant `setuphelpers`, `win32ts` et `WAPT.write_audit_data_if_changed`. +- Une session utilisateur active et des données RSoP accessibles pour le compte ciblé. +- Droits suffisants dans le contexte d'exécution de l'agent pour lire ces données et créer un fichier temporaire. +La présence d'un compte actif ne garantit pas la disponibilité de ses données RSoP. Un compte local est détectable, mais ne dispose pas nécessairement de données de stratégies exploitables. +**Résultat d'audit** +**Catégorie :** `audit-gpresult` — **Clé :** `user-gpo`. +Exemple illustratif : +``` +{ +   "status": "OK", +   "summary": { +     "total_gpo_count": 2, +     "denied_gpo_count": 1, +     "applied_gpo_count": 1 +   }, +   "computer": { +     "name": "POSTE01" +   }, +   "user": { +     "account": "DOMAINE\\utilisateur", +     "name": "utilisateur", +     "domain": "DOMAINE" +   }, +   "denied_gpos": ["Local Group Policy"], +   "applied_gpos": ["GPO Utilisateurs"] + } +  +``` + +| | | +| - | - | +| **Champ** | **Signification** | +| `status` | État de la collecte, pas un verdict de conformité des GPO. | +| `summary.total_gpo_count` | Nombre d'entrées GPO analysées dans `UserResults`. | +| `summary.applied_gpo_count` | Nombre d'entrées classées comme appliquées. | +| `summary.denied_gpo_count` | Nombre d'entrées classées comme non appliquées. | +| `computer.name` | Nom du poste remonté par `COMPUTERNAME`. | +| `user.account` | Compte complet sélectionné via WTS. | +| `user.name` | Nom d'utilisateur. | +| `user.domain` | Domaine fourni par WTS, généralement NetBIOS ; ce n'est pas nécessairement le nom DNS du domaine AD. | +| `applied_gpos` | Liste triée des noms des GPO appliquées. | +| `denied_gpos` | Liste triée des noms des GPO non appliquées. | + +  +**Convention :** `denied_gpos` inclut les GPO non appliquées pour toutes les causes reconnues par les indicateurs XML, notamment le filtrage, la désactivation ou l'invalidité. Cela peut inclure une GPO vide ; cette liste ne signifie donc pas uniquement « refus de sécurité ». Si le nom manque, l'identifiant XML est utilisé lorsqu'il est disponible. +Le classement conserve exactement la logique de la version retenue : un indicateur `Enabled`, `IsValid` ou `FilterAllowed` à faux, ou `AccessDenied` à vrai, classe la GPO comme non appliquée. Sinon, `IsValid` et `FilterAllowed` doivent être vrais pour la classer comme appliquée. Un état indéterminable déclenche une erreur. +**Statuts et cas particuliers** + +| | | +| - | - | +| **Statut** | **Situation** | +| `OK` | Export XML et analyse terminés, résultat enregistré. Des GPO non appliquées peuvent être présentes. | +| `WARNING` | Aucun utilisateur avec une session active. | +| `ERROR` | Plusieurs comptes actifs, échec WTS/GPResult, XML absent ou inexploitable, ou état de GPO indéterminable. | + +  +Les sessions déconnectées sont ignorées. Plusieurs sessions actives du même compte sont regroupées ; plusieurs comptes actifs distincts provoquent une erreur pour éviter une sélection arbitraire. +En cas de `WARNING` ou d'`ERROR`, le résultat de la même clé est remplacé par un objet de statut et un message. Les listes et compteurs du précédent succès ne sont pas recopiés. Le champ `user` vaut alors `null` ou contient le compte ciblé sous forme de chaîne, selon le cas ; les consommateurs doivent consulter `status` avant de lire les champs du résultat de succès. +Les paramètres d'enregistrement sont `keep_days=365` et `max_count=1`. La collecte ne crée pas une clé distincte par utilisateur : elle représente le résultat du dernier audit du poste, selon le compte actif au moment de l'exécution. +**Mise en place et exploitation** +1. Remplacer le `setup.py` du paquet existant par le fichier fourni. +2. Reporter la description proposée dans `WAPT/control` et incrémenter la version du paquet selon les conventions du dépôt. +3. Reconstruire, signer et téléverser le paquet dans le dépôt WAPT. +4. Déployer la nouvelle version sur un poste avec une session utilisateur active. +5. Lancer l'audit du paquet depuis WAPT et consulter `audit-gpresult.user-gpo`. +La fréquence de réexécution dépend de la configuration d'audit WAPT existante ; ce code n'ajoute aucun mécanisme de planification. +**Dépannage** +Le journal indique le compte ciblé, la commande et la présence du dossier temporaire. Le délai maximal de la commande est de 120 secondes. +- **Aucun utilisateur actif :** ouvrir une session, puis relancer l'audit. +- **Plusieurs utilisateurs actifs :** ne conserver qu'un seul compte actif sur le poste, puis relancer l'audit. +- **Erreur GPResult :** consulter la sortie de la commande dans le journal ; vérifier les droits, les données RSoP et l'accès au dossier temporaire. +- **Erreur XML ou état indéterminable :** récupérer un export XML produit dans les mêmes conditions pour examiner la structure avant de modifier le parseur. +L'appel direct à `gpresult.exe` est conservé dans cette version ; aucun chemin `Sysnative` n'est forcé. +**Contenu** +- `setup.py` : version retenue, enrichie uniquement de commentaires et de docstrings. +- `README.md` : identification, fonctionnement, données et exploitation. +- `WAPT/description.txt` : ligne de description à reporter dans le contrôle existant. diff --git a/WAPT/description.txt b/WAPT/description.txt new file mode 100644 index 0000000..085607e --- /dev/null +++ b/WAPT/description.txt @@ -0,0 +1 @@ +description: Audit des GPO de l'utilisateur actif : identification du compte, listes des GPO appliquees et non appliquees et compteurs dans audit-gpresult.user-gpo. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..be20796 --- /dev/null +++ b/setup.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +"""Audit des GPO de l'utilisateur actif : audit-gpresult.user-gpo. + +L'installation ne realise aucune action. La collecte est executee dans +audit(), sous le compte de l'agent WAPT, en ciblant explicitement les +donnees RSoP de l'utilisateur avec l'option /USER de gpresult.exe. +""" + +import os +import tempfile +import xml.etree.ElementTree as ET + +from setuphelpers import * + + +def install(): + """Point d'entree obligatoire ; aucune configuration a appliquer.""" + pass + + +def _get_logged_on_user(): + """Retourne DOMAINE\\utilisateur pour l'unique utilisateur actif. + + Les sessions deconnectees sont ignorees. Plusieurs comptes actifs + provoquent une erreur pour eviter de choisir une cible arbitraire. + Sans utilisateur actif, retourne None. + """ + import win32ts + + server = win32ts.WTS_CURRENT_SERVER_HANDLE + users = [] + + for session in win32ts.WTSEnumerateSessions(server, 1, 0): + if session["State"] != win32ts.WTSActive: + continue + + session_id = session["SessionId"] + + username = win32ts.WTSQuerySessionInformation( + server, session_id, win32ts.WTSUserName + ) + domain = win32ts.WTSQuerySessionInformation( + server, session_id, win32ts.WTSDomainName + ) + + username = (username or "").strip() + domain = (domain or "").strip() + + if not username: + continue + + account = ( + "%s\\%s" % (domain, username) + if domain else username + ) + + if account not in users: + users.append(account) + + if len(users) > 1: + raise Exception( + "Plusieurs utilisateurs ont une session active : %s" + % ", ".join(users) + ) + + return users[0] if users else None + + +def _write_user_audit(data): + """Enregistre un objet structure, sans conversion en chaine JSON. + + La meme cle est utilisee pour les succes, avertissements et erreurs, + afin de ne pas laisser un ancien resultat presente comme actuel. + """ + WAPT.write_audit_data_if_changed( + "audit-gpresult", + "user-gpo", + data, + keep_days=365, + max_count=1 + ) + + +def _xml_flag(element, tag): + """Lit un booleen XML ; None signifie que le champ est absent.""" + value = element.findtext(tag) + + if value is None: + return None + + value = value.strip().lower() + + if value in ("true", "1"): + return True + + if value in ("false", "0"): + return False + + raise Exception( + "Valeur XML inattendue pour %s : %s" % (tag, value) + ) + + +def _parse_user_gpos(xml_path): + """Retourne les listes triees des GPO appliquees et non appliquees. + + denied_gpos regroupe toutes les GPO non appliquees selon les indicateurs + XML : desactivation, invalidite, filtrage ou refus d'acces. + """ + root = ET.parse(xml_path).getroot() + + # Neutraliser les espaces de noms pour lire les balises par leur nom. + for element in root.iter(): + if isinstance(element.tag, str): + element.tag = element.tag.rsplit("}", 1)[-1] + + user_results = root.find(".//UserResults") + + if user_results is None: + raise Exception( + "Le rapport XML ne contient aucune section UserResults." + ) + + applied = [] + denied = [] + + # Les references imbriquees ne sont pas recomptees comme des GPO. + for gpo in user_results.findall("GPO"): + name = ( + gpo.findtext("Name") + or gpo.findtext("Path/Identifier") + or "" + ).strip() + + if not name: + raise Exception( + "GPO sans nom ni identifiant dans le rapport XML." + ) + + enabled = _xml_flag(gpo, "Enabled") + valid = _xml_flag(gpo, "IsValid") + allowed = _xml_flag(gpo, "FilterAllowed") + access_denied = _xml_flag(gpo, "AccessDenied") + + if ( + enabled is False + or valid is False + or allowed is False + or access_denied is True + ): + denied.append(name) + + elif valid is True and allowed is True: + applied.append(name) + + else: + # Un etat inconnu ne doit pas devenir un faux succes. + raise Exception( + "Etat de la GPO indeterminable : %s" % name + ) + + return sorted(applied), sorted(denied) + + +def audit(): + """Collecte le GPResult utilisateur et retourne OK, WARNING ou ERROR.""" + user = None + computer = { + "name": os.environ.get("COMPUTERNAME", "") + } + + try: + user = _get_logged_on_user() + + if not user: + data = { + "status": "WARNING", + "message": "Aucun utilisateur avec une session active.", + "computer": computer, + "user": None + } + + _write_user_audit(data) + print(data["message"]) + return "WARNING" + + print("Audit GPResult utilisateur : %s" % user) + + # Dossier unique, automatiquement supprime a la sortie du bloc, + # y compris si l'export ou l'analyse XML echoue. + with tempfile.TemporaryDirectory( + prefix="wapt_gpresult_user_" + ) as temp_dir: + xml_path = os.path.join(temp_dir, "gpresult.xml") + + # Conserver l'appel direct valide sur l'environnement cible. + # /USER cible les donnees de l'utilisateur ; /SCOPE USER + # exclut les GPO machine ; /X exporte les donnees en XML. + command = ( + 'gpresult.exe /USER "%s" /SCOPE USER /X "%s" /F' + % (user, xml_path) + ) + + print("Commande : %s" % command) + print( + "Dossier temporaire present : %s" + % os.path.isdir(temp_dir) + ) + + result = run( + command, + timeout=120, + return_stderr=True + ) + + if result: + print(result) + + if not os.path.isfile(xml_path): + raise Exception( + "GPResult n'a pas genere de rapport XML : %s" + % xml_path + ) + + applied, denied = _parse_user_gpos(xml_path) + + # Le domaine provient du compte WTS (souvent au format NetBIOS). + domain, separator, username = user.partition("\\") + + if not separator: + username = user + domain = "" + + data = { + "status": "OK", + "summary": { + "total_gpo_count": len(applied) + len(denied), + "denied_gpo_count": len(denied), + "applied_gpo_count": len(applied) + }, + "computer": computer, + "user": { + "account": user, + "name": username, + "domain": domain + }, + "denied_gpos": denied, + "applied_gpos": applied + } + + _write_user_audit(data) + + print( + "Audit disponible : audit-gpresult.user-gpo " + "(%s appliquees, %s non appliquees)" + % (len(applied), len(denied)) + ) + + return "OK" + + except Exception as e: + # Remplacer le resultat precedent par l'erreur de cette collecte. + data = { + "status": "ERROR", + "message": str(e), + "computer": computer, + "user": user + } + + _write_user_audit(data) + print("Erreur audit GPResult utilisateur : %s" % e) + return "ERROR"