1er upload

This commit is contained in:
slaporte committed 2026-10-28 16:45:44 +01:00
1 parent a20caf3b51
commit 010f45b8cf
3 files changed
+375 -2

No files matched your search

+102 -2
View File
@@ -1,3 +1,103 @@
# comi-audit-gpresult-user
**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 "<fichier temporaire>" /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"]
 }
 
```
affiche le gpresult du user actif dans les données d'audit
| | |
| - | - |
| **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.
+1
View File
@@ -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.
+272
View File
@@ -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"