Compare commits

..
3 Commits
3 changed files with 789 additions and 284 deletions

No files matched your search

+140 -97
View File
@@ -1,103 +1,146 @@
**form-gpresult-user** # Audit GPResult — Utilisateur actif
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** Paquet WAPT d'audit des GPO et des groupes de sécurité de l'utilisateur actif. Le résultat structuré est enregistré dans **`audit-gpresult.user-gpo`**.
- **Nom du paquet :** `form-gpresult-user`
- **Libellé :** Audit GPO — Utilisateur actif ## Fonctionnement
- **Description proposée pour le champ `description` de `WAPT/control` :**
``` 1. `install()` ne réalise aucune action.
Audit des GPO de l'utilisateur actif : identification du compte, listes des GPO appliquees et non appliquees et compteurs dans audit-gpresult.user-gpo. 2. `audit()` appelle `quser.exe` avec un délai maximal de 30 secondes. Le script retient le premier utilisateur dont la ligne contient l'état `Active` ou `Actif`.
  3. Si aucun utilisateur n'est détecté, il enregistre `NO_USER` et retourne `OK` à WAPT.
``` 4. Sinon, il supprime un éventuel rapport précédent et lance GPResult avec un délai maximal de 120 secondes :
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** ```text
1. L'installation appelle `install()`, volontairement vide. gpresult.exe /USER "<utilisateur détecté>" /SCOPE USER /X "<rapport.xml>" /F
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. 5. Il analyse `UserResults`, extrait l'identité du compte, classe les GPO et les groupes de sécurité.
``` 6. Il transmet un dictionnaire à `WAPT.write_audit_data_if_changed`, avec `keep_days=365` et `max_count=1`.
gpresult.exe /USER "DOMAINE\utilisateur" /SCOPE USER /X "<fichier temporaire>" /F 7. La suppression du rapport temporaire est tentée dans `finally`, après succès comme après erreur.
 
``` Le chemin de GPResult est construit avec `makepath(system32(), "gpresult.exe")`. L'audit s'exécute dans le contexte de l'agent WAPT et cible explicitement le RSoP utilisateur. Il n'utilise pas `session_setup()`, ne lance pas de `gpupdate` et ne modifie pas les stratégies.
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. ## Prérequis et sélection de la session
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. - Poste Windows disposant de `quser.exe`, de GPResult et de données RSoP accessibles pour l'utilisateur ciblé.
**Prérequis** - Agent WAPT fournissant `setuphelpers` et `WAPT.write_audit_data_if_changed`.
- Poste Windows disposant de `gpresult.exe`, accessible depuis le contexte de l'agent. - Droits suffisants pour consulter les sessions et les données RSoP, ainsi que pour écrire dans le répertoire temporaire.
- 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é. La détection utilise la sortie texte de `quser`, et non les API WTS ou `win32ts`. Seuls les libellés `Active` et `Actif` sont reconnus. Les sessions déconnectées sont ignorées. Si plusieurs utilisateurs sont actifs, le premier reconnu dans la sortie est sélectionné ; aucun contrôle d'unicité n'est effectué.
- 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. Seul le nom d'utilisateur détecté est transmis à `/USER`. Le script lit ensuite le premier texte `Name` de `UserResults` comme identité RSoP, avec repli sur le nom détecté si ce champ est absent. Sur un poste comportant des comptes homonymes, vérifier l'identité effectivement remontée.
**Résultat d'audit**
**Catégorie :** `audit-gpresult` — **Clé :** `user-gpo`. Une erreur de `quser`, une sortie vide ou un format non reconnu aboutit aussi à `NO_USER`. Consulter le journal pour distinguer une erreur de détection d'une absence réelle de session. Une session active, locale ou de domaine, ne garantit pas que ses données RSoP soient disponibles.
Exemple illustratif :
``` ## Données produites
Catégorie : `audit-gpresult` ; clé : `user-gpo`.
Exemple illustratif d'un résultat de succès :
```json
{ {
   "status": "OK", "status": "OK",
   "summary": { "user": {
     "total_gpo_count": 2, "account": "EXEMPLE\\utilisateur",
     "denied_gpo_count": 1, "username": "utilisateur",
     "applied_gpo_count": 1 "account_domain": "EXEMPLE",
   }, "account_type": "domain"
   "computer": { },
     "name": "POSTE01" "summary": {
   }, "applied_gpo_count": 1,
   "user": { "denied_gpo_count": 1,
     "account": "DOMAINE\\utilisateur", "total_gpo_count": 2,
     "name": "utilisateur", "domain_group_count": 1,
     "domain": "DOMAINE" "local_group_count": 1,
   }, "system_group_count": 1,
   "denied_gpos": ["Local Group Policy"], "other_group_count": 0
   "applied_gpos": ["GPO Utilisateurs"] },
 } "applied_gpos": ["GPO Utilisateurs"],
  "denied_gpos": ["GPO non appliquee"],
"domain_groups": ["EXEMPLE\\Domain Users"],
"security_groups": {
"domain": ["EXEMPLE\\Domain Users"],
"local": ["BUILTIN\\Users"],
"system": ["NT AUTHORITY\\INTERACTIVE"],
"other": []
}
}
``` ```
| | | | Champ | Signification |
| - | - | | --- | --- |
| **Champ** | **Signification** | | `status` | État de la collecte ; ce n'est pas un verdict de conformité. |
| `status` | État de la collecte, pas un verdict de conformité des GPO. | | `user.account` | Identité lue dans le RSoP, ou nom détecté si elle est absente. |
| `summary.total_gpo_count` | Nombre d'entrées GPO analysées dans `UserResults`. | | `user.username` | Partie du compte après le premier `\`, ou compte entier sans préfixe. |
| `summary.applied_gpo_count` | Nombre d'entrées classées comme appliquées. | | `user.account_domain` | Préfixe du compte, généralement NetBIOS pour un domaine ou nom du poste pour un compte local ; vide sans préfixe. |
| `summary.denied_gpo_count` | Nombre d'entrées classées comme non appliquées. | | `user.account_type` | `local` si le préfixe est vide ou égal au nom du poste ; `domain` sinon. |
| `computer.name` | Nom du poste remonté par `COMPUTERNAME`. | | `summary.applied_gpo_count` / `summary.denied_gpo_count` | Nombre de GPO retenues dans chaque liste. |
| `user.account` | Compte complet sélectionné via WTS. | | `summary.total_gpo_count` | Somme des deux compteurs de GPO. |
| `user.name` | Nom d'utilisateur. | | `summary.domain_group_count` / `summary.local_group_count` / `summary.system_group_count` / `summary.other_group_count` | Nombre de groupes retenus dans chaque catégorie. |
| `user.domain` | Domaine fourni par WTS, généralement NetBIOS ; ce n'est pas nécessairement le nom DNS du domaine AD. | | `applied_gpos` / `denied_gpos` | Noms des GPO, dans l'ordre de collecte. |
| `applied_gpos` | Liste triée des noms des GPO appliquées. | | `domain_groups` | Même liste que `security_groups.domain`, pour faciliter le reporting. |
| `denied_gpos` | Liste triée des noms des GPO non appliquées. | | `security_groups` | Groupes répartis entre `domain`, `local`, `system` et `other`. |
  La recherche de texte est récursive et ignore les namespaces XML. Le type de compte est déduit du préfixe ; il n'est pas validé par une requête AD.
**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**
| | | ### Évolution du schéma
| - | - |
| **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 consommateurs de l'ancien résultat doivent utiliser `user.username` à la place de `user.name` et `user.account_domain` à la place de `user.domain`. Le résultat actuel ne contient plus de bloc `computer`. `user.account_type`, les listes de groupes et leurs compteurs sont ajoutés.
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. ## Classement des GPO
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** Une GPO est appliquée si `Enabled`, `IsValid` et `FilterAllowed` sont vrais et si `AccessDenied` est faux. Toutes les autres entrées retenues vont dans `denied_gpos` : cette liste couvre les GPO non appliquées, y compris les GPO filtrées, désactivées ou invalides.
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. Les valeurs `true`, `1`, `yes` et `oui`, sans distinction de casse, sont vraies. Toute autre valeur ou un champ absent est faux. Un indicateur positif absent empêche donc le classement comme GPO appliquée ; un `AccessDenied` absent est considéré comme faux. Aucun état indéterminable ne déclenche d'erreur spécifique dans cette version.
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. La recherche des balises `GPO` est récursive. Les entrées sans nom sont ignorées, sans repli sur un identifiant XML. Les doublons sont éliminés selon `(nom, VersionDirectory, VersionSysvol)` ; un nom peut apparaître plusieurs fois si les versions diffèrent. Les listes de GPO ne sont pas triées. Les compteurs décrivent les entrées retenues, sans constituer un inventaire de toutes les GPO du domaine.
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. ## Classement des groupes de sécurité
**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. Les groupes sont extraits des balises `SecurityGroup` du RSoP, sans requête LDAP supplémentaire. Le classement utilise les préfixes des noms, dans cet ordre :
- **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. | Catégorie | Règle |
- **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. | `system` | Préfixe `NT AUTHORITY\`. |
L'appel direct à `gpresult.exe` est conservé dans cette version ; aucun chemin `Sysnative` n'est forcé. | `local` | Préfixe `BUILTIN\` ou nom du poste suivi de `\`. |
**Contenu** | `domain` | Préfixe du compte RSoP suivi de `\`, si ce préfixe est non vide et différent du nom du poste. |
- `setup.py` : version retenue, enrichie uniquement de commentaires et de docstrings. | `other` | Toutes les autres identités. |
- `README.md` : identification, fonctionnement, données et exploitation.
- `WAPT/description.txt` : ligne de description à reporter dans le contrôle existant. Un compte `POSTE01\utilisateur` ne fournit donc pas de préfixe AD pour classer les groupes de domaine. Les comparaisons et la déduplication ignorent la casse. Les entrées sans nom sont ignorées et chaque liste est triée alphabétiquement sans distinction de casse.
Ce classement repose sur les noms de l'export, sans validation par SID ou LDAP. Les noms localisés ou les groupes d'un autre domaine peuvent rester dans `other`. Les listes reflètent les données RSoP collectées ; elles ne constituent pas un inventaire exhaustif des groupes AD ou locaux.
## Statuts et cas particuliers
| Statut enregistré | Retour WAPT | Situation |
| --- | --- | --- |
| `OK` | `OK` | Export, analyse et enregistrement terminés, même avec des GPO non appliquées ou des listes vides. |
| `NO_USER` | `OK` | Aucun utilisateur actif reconnu, y compris en cas d'échec de détection capturé. |
| `ERROR` | `ERROR` | Exception capturée par l'audit : GPResult en échec, fichier absent, XML invalide, section `UserResults` absente, etc. |
Le résultat `NO_USER` contient `message`, `user: null`, des listes `applied_gpos`, `denied_gpos` et `domain_groups` vides, ainsi que les quatre catégories vides de `security_groups`. Il ne contient pas de bloc `summary`. Aucun statut `WARNING` n'est utilisé.
En cas d'erreur, la même clé reçoit uniquement `status: ERROR` et `message`. Les listes et compteurs du précédent succès ne sont pas recopiés. Les consommateurs doivent lire `status` avant d'accéder aux champs de succès.
La clé représente le dernier audit du poste, selon l'utilisateur sélectionné au moment de l'exécution ; aucune clé distincte par utilisateur n'est créée.
## Déploiement et dépannage
1. Mettre à jour `setup.py` dans les sources du paquet existant.
2. Adapter si nécessaire la description de `WAPT/control` pour mentionner les groupes de sécurité, en conservant l'identité et les autres métadonnées du paquet ; incrémenter sa version selon les conventions du dépôt.
3. Reconstruire, signer, téléverser et déployer le paquet.
4. Lancer l'audit avec une session active et consulter `audit-gpresult.user-gpo`.
La fréquence des audits dépend de la configuration WAPT ; le script ne crée pas de planification. Le journal indique l'utilisateur détecté, le compte RSoP, son type, les compteurs de GPO et de groupes de domaine ou l'erreur rencontrée.
- **`NO_USER` inattendu :** vérifier la sortie de `quser.exe`, les libellés reconnus et les éventuelles erreurs de détection dans le journal.
- **Compte inattendu :** vérifier l'ordre des sessions actives et comparer le nom détecté avec `user.account`.
- **Erreur GPResult ou XML :** vérifier les droits de l'agent, la disponibilité du RSoP pour ce compte, l'accès à GPResult et au répertoire temporaire.
Le rapport utilise le nom fixe `wapt-gpresult-user.xml` dans `tempfile.gettempdir()`. Éviter les audits simultanés partageant ce répertoire. Un échec de nettoyage est ignoré et peut laisser le fichier sur disque.
## Contenu du dépôt
- `setup.py` : détection de session, collecte et analyse des GPO et groupes de sécurité utilisateur.
- `README.md` : fonctionnement, schéma de données et exploitation.
- `WAPT/description.txt` : description fournie pour le contrôle existant ; elle décrit les GPO et ne mentionne pas encore les groupes de sécurité. Ce fichier ne remplace pas un `WAPT/control` complet.
+1 -1
View File
@@ -1 +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. description: Audit des GPO de l'utilisateur actif : identification du compte, listes des GPO appliquees et non appliquees, groupes et compteurs dans audit-gpresult.user-gpo.
+648 -186
View File
@@ -1,272 +1,734 @@
# -*- coding: utf-8 -*- # -*- 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 * from setuphelpers import *
import os
import re
import tempfile
import xml.etree.ElementTree as ET
# ---------------------------------------------------------------------------
# Identifiants de l'audit WAPT
# ---------------------------------------------------------------------------
AUDIT_SECTION = "audit-gpresult"
AUDIT_NAME = "user-gpo"
def install(): def install():
"""Point d'entree obligatoire ; aucune configuration a appliquer.""" """
Ce paquet est uniquement destiné à l'audit.
Aucune modification persistante n'est effectuée sur le poste.
"""
pass pass
# ---------------------------------------------------------------------------
# Fonctions utilitaires XML
# ---------------------------------------------------------------------------
def _clean_tag(tag):
"""
Supprime le namespace XML d'un tag ElementTree.
"""
return tag.split("}")[-1]
def _text(element, tag, default=""):
"""
Retourne récursivement la première valeur XML correspondant au tag
demandé, en ignorant les namespaces.
"""
if element is None:
return default
for child in element.iter():
if _clean_tag(child.tag).lower() == tag.lower():
if child.text:
return child.text.strip()
return default
def _to_bool(value):
"""
Convertit une valeur booléenne textuelle issue de GPResult
en booléen Python.
"""
return str(value).strip().lower() in (
"true",
"1",
"yes",
"oui"
)
def _split_account(account):
"""
Sépare un compte Windows entre son préfixe et son nom de compte.
Exemples :
FORMATION\\sebastien
-> ("FORMATION", "sebastien")
WSMANAGE30\\formation
-> ("WSMANAGE30", "formation")
"""
if not account:
return "", ""
if "\\" in account:
return account.split("\\", 1)
return "", account
# ---------------------------------------------------------------------------
# Détection de l'utilisateur interactif
# ---------------------------------------------------------------------------
def _get_logged_on_user(): 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 Retourne le nom de l'utilisateur associé à la session interactive active.
server = win32ts.WTS_CURRENT_SERVER_HANDLE Les audits WAPT s'exécutent dans le contexte du service WAPT.
users = [] Il n'est donc pas possible de se fier simplement à l'identité
du processus courant.
for session in win32ts.WTSEnumerateSessions(server, 1, 0): `quser` est utilisé pour inspecter les sessions Windows et récupérer
if session["State"] != win32ts.WTSActive: l'utilisateur de la session interactive active.
continue
session_id = session["SessionId"] Exemples retournés par cette fonction :
formation
sebastien
username = win32ts.WTSQuerySessionInformation( L'identité complète du compte est ensuite résolue par GPResult :
server, session_id, win32ts.WTSUserName WSMANAGE30\\formation
) FORMATION\\sebastien
domain = win32ts.WTSQuerySessionInformation(
server, session_id, win32ts.WTSDomainName
)
username = (username or "").strip() Returns:
domain = (domain or "").strip() str ou None
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", try:
"user-gpo",
data, output = run(
keep_days=365, r'quser.exe',
max_count=1 timeout=30,
return_stderr=False
) )
if not output:
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 return None
value = value.strip().lower() lines = output.splitlines()
if value in ("true", "1"): # La première ligne correspond aux en-têtes de colonnes.
return True for line in lines[1:]:
if value in ("false", "0"): line = line.strip()
return False
raise Exception( if not line:
"Valeur XML inattendue pour %s : %s" % (tag, value) continue
# quser peut préfixer la session courante avec ">".
line = line.lstrip(">").strip()
# Prend en charge les sorties Windows en anglais et en français.
if (
" Active " not in " %s " % line
and
" Actif " not in " %s " % line
):
continue
parts = re.split(
r"\s+",
line
) )
if not parts:
continue
def _parse_user_gpos(xml_path): # USERNAME correspond à la première colonne.
"""Retourne les listes triees des GPO appliquees et non appliquees. username = parts[0].strip()
denied_gpos regroupe toutes les GPO non appliquees selon les indicateurs if username:
XML : desactivation, invalidite, filtrage ou refus d'acces. return username
return None
except Exception as e:
print(
"Unable to detect interactive user: %s"
% e
)
return None
# ---------------------------------------------------------------------------
# Recherche des données GPResult dans le XML
# ---------------------------------------------------------------------------
def _find_user_results(root):
"""
Recherche la section UserResults dans le document XML GPResult.
""" """
root = ET.parse(xml_path).getroot()
# Neutraliser les espaces de noms pour lire les balises par leur nom.
for element in root.iter(): for element in root.iter():
if isinstance(element.tag, str):
element.tag = element.tag.rsplit("}", 1)[-1]
user_results = root.find(".//UserResults") if _clean_tag(element.tag) == "UserResults":
return element
if user_results is None: return None
raise Exception(
"Le rapport XML ne contient aucune section UserResults."
) # ---------------------------------------------------------------------------
# Analyse des GPO
# ---------------------------------------------------------------------------
def _get_gpos(scope_results):
"""
Extrait les noms des GPO appliquées et refusées depuis le RSoP USER.
Les structures GPO internes sans nom sont ignorées
et les doublons sont supprimés.
"""
applied = [] applied = []
denied = [] denied = []
# Les references imbriquees ne sont pas recomptees comme des GPO. seen = set()
for gpo in user_results.findall("GPO"):
name = (
gpo.findtext("Name")
or gpo.findtext("Path/Identifier")
or ""
).strip()
if not name: if scope_results is None:
raise Exception( return applied, denied
"GPO sans nom ni identifiant dans le rapport XML."
for element in scope_results.iter():
if _clean_tag(element.tag) != "GPO":
continue
name = _text(
element,
"Name"
) )
enabled = _xml_flag(gpo, "Enabled") if not name:
valid = _xml_flag(gpo, "IsValid") continue
allowed = _xml_flag(gpo, "FilterAllowed")
access_denied = _xml_flag(gpo, "AccessDenied") enabled = _to_bool(
_text(element, "Enabled")
)
valid = _to_bool(
_text(element, "IsValid")
)
filter_allowed = _to_bool(
_text(element, "FilterAllowed")
)
access_denied = _to_bool(
_text(element, "AccessDenied")
)
version_directory = _text(
element,
"VersionDirectory"
)
version_sysvol = _text(
element,
"VersionSysvol"
)
identity = (
name,
version_directory,
version_sysvol
)
if identity in seen:
continue
seen.add(identity)
if ( if (
enabled is False enabled
or valid is False and valid
or allowed is False and filter_allowed
or access_denied is True and not access_denied
): ):
denied.append(name)
elif valid is True and allowed is True:
applied.append(name) applied.append(name)
else: else:
# Un etat inconnu ne doit pas devenir un faux succes.
raise Exception( denied.append(name)
"Etat de la GPO indeterminable : %s" % name
return applied, denied
# ---------------------------------------------------------------------------
# Analyse des groupes de sécurité
# ---------------------------------------------------------------------------
def _get_security_groups(scope_results, principal_name):
"""
Extrait et classe les groupes de sécurité utilisés dans le RSoP USER.
Catégories :
domain:
Groupes dont le préfixe correspond au préfixe de domaine
du compte utilisateur.
Exemple :
FORMATION\\Domain Users
local:
Groupes BUILTIN ou groupes appartenant au poste local.
Exemples :
BUILTIN\\Users
WSMANAGE30\\Administrators
system:
Identités NT AUTHORITY.
Exemples :
NT AUTHORITY\\Authenticated Users
NT AUTHORITY\\INTERACTIVE
other:
Identités de sécurité qui ne peuvent pas être classées
de manière fiable.
Cette distinction évite notamment qu'un compte local tel que :
WSMANAGE30\\formation
soit considéré à tort comme un compte Active Directory.
"""
groups = {
"domain": [],
"local": [],
"system": [],
"other": [],
}
seen = set()
if scope_results is None:
return groups
principal_domain, _ = _split_account(
principal_name
) )
return sorted(applied), sorted(denied) principal_domain = principal_domain.upper()
computer_name = os.environ.get(
"COMPUTERNAME",
""
).upper()
# Le préfixe du compte n'est considéré comme un domaine AD
# que s'il est différent du nom du poste local.
ad_domain = ""
if (
principal_domain
and principal_domain != computer_name
):
ad_domain = principal_domain
for element in scope_results.iter():
if _clean_tag(element.tag) != "SecurityGroup":
continue
name = _text(
element,
"Name"
)
if not name:
continue
name = name.strip()
key = name.lower()
if key in seen:
continue
seen.add(key)
upper_name = name.upper()
if upper_name.startswith(
"NT AUTHORITY\\"
):
groups["system"].append(name)
elif upper_name.startswith(
"BUILTIN\\"
):
groups["local"].append(name)
elif (
computer_name
and upper_name.startswith(
computer_name + "\\"
)
):
groups["local"].append(name)
elif (
ad_domain
and upper_name.startswith(
ad_domain + "\\"
)
):
groups["domain"].append(name)
else:
groups["other"].append(name)
# Garantit un ordre stable dans les résultats.
for category in groups:
groups[category] = sorted(
groups[category],
key=lambda x: x.lower()
)
return groups
# ---------------------------------------------------------------------------
# Audit WAPT
# ---------------------------------------------------------------------------
def audit(): def audit():
"""Collecte le GPResult utilisateur et retourne OK, WARNING ou ERROR.""" """
user = None Exécute GPResult pour le contexte USER de l'utilisateur interactif actif.
computer = {
"name": os.environ.get("COMPUTERNAME", "")
}
try: Déroulement :
user = _get_logged_on_user() 1. Détecte la session utilisateur Windows active.
2. Exécute GPResult pour le RSoP USER de ce compte.
3. Analyse les GPO appliquées et refusées.
4. Extrait et classe les groupes de sécurité.
5. Publie le résultat structuré dans les données d'audit WAPT.
if not user: L'absence d'utilisateur interactif est considérée comme une situation
data = { normale. Dans ce cas, l'audit WAPT retourne OK avec un statut
"status": "WARNING", fonctionnel NO_USER.
"message": "Aucun utilisateur avec une session active.", """
"computer": computer,
"user": None
}
_write_user_audit(data) xml_file = os.path.join(
print(data["message"]) tempfile.gettempdir(),
return "WARNING" "wapt-gpresult-user.xml"
)
print("Audit GPResult utilisateur : %s" % user)
try:
# Dossier unique, automatiquement supprime a la sortie du bloc,
# y compris si l'export ou l'analyse XML echoue. # WAPT s'exécute en tant que service.
with tempfile.TemporaryDirectory( # Il faut donc identifier explicitement l'utilisateur interactif.
prefix="wapt_gpresult_user_" logged_on_user = _get_logged_on_user()
) as temp_dir:
xml_path = os.path.join(temp_dir, "gpresult.xml") if not logged_on_user:
# Conserver l'appel direct valide sur l'environnement cible. audit_data = {
# /USER cible les donnees de l'utilisateur ; /SCOPE USER
# exclut les GPO machine ; /X exporte les donnees en XML. "status": "NO_USER",
command = (
'gpresult.exe /USER "%s" /SCOPE USER /X "%s" /F' "message":
% (user, xml_path) "No active interactive user session",
"user": None,
"applied_gpos": [],
"denied_gpos": [],
"domain_groups": [],
"security_groups": {
"domain": [],
"local": [],
"system": [],
"other": [],
}
}
WAPT.write_audit_data_if_changed(
AUDIT_SECTION,
AUDIT_NAME,
audit_data,
keep_days=365,
max_count=1
) )
print("Commande : %s" % command)
print( print(
"Dossier temporaire present : %s" "No active interactive user session"
% os.path.isdir(temp_dir) )
# L'absence d'utilisateur connecté n'est pas considérée
# comme un échec de l'audit.
return "OK"
print(
"Active interactive user: %s"
% logged_on_user
)
# Supprime un éventuel rapport restant d'une exécution précédente.
if os.path.isfile(xml_file):
os.remove(xml_file)
gpresult_path = makepath(
system32(),
"gpresult.exe"
)
# Le nom utilisateur retourné par quser suffit à GPResult
# pour résoudre le compte local ou de domaine réellement associé
# au RSoP.
command = (
'"%s" '
'/USER "%s" '
'/SCOPE USER '
'/X "%s" '
'/F'
% (
gpresult_path,
logged_on_user,
xml_file
)
)
print(
"Running GPResult USER for %s..."
% logged_on_user
) )
result = run( result = run(
command, command,
timeout=120, timeout=120,
return_stderr=True return_stderr=False
) )
if result: if not os.path.isfile(xml_file):
print(result)
if not os.path.isfile(xml_path):
raise Exception( raise Exception(
"GPResult n'a pas genere de rapport XML : %s" "GPResult did not generate XML report. "
% xml_path "Output: %s"
% result
) )
applied, denied = _parse_user_gpos(xml_path) tree = ET.parse(
xml_file
)
# Le domaine provient du compte WTS (souvent au format NetBIOS). root = tree.getroot()
domain, separator, username = user.partition("\\")
if not separator: user_results = _find_user_results(
username = user root
domain = "" )
if user_results is None:
raise Exception(
"UserResults section not found"
)
applied_gpos, denied_gpos = _get_gpos(
user_results
)
# On laisse GPResult fournir l'identité de compte faisant foi.
#
# Exemples :
# FORMATION\\sebastien
# WSMANAGE30\\formation
rsop_user = _text(
user_results,
"Name"
)
if not rsop_user:
rsop_user = logged_on_user
account_domain, username = _split_account(
rsop_user
)
security_groups = _get_security_groups(
user_results,
rsop_user
)
computer_name = os.environ.get(
"COMPUTERNAME",
""
)
# Détermine si GPResult a résolu le compte
# comme compte local ou compte de domaine.
if (
not account_domain
or account_domain.upper()
== computer_name.upper()
):
account_type = "local"
else:
account_type = "domain"
audit_data = {
data = {
"status": "OK", "status": "OK",
"summary": {
"total_gpo_count": len(applied) + len(denied),
"denied_gpo_count": len(denied),
"applied_gpo_count": len(applied)
},
"computer": computer,
"user": { "user": {
"account": user,
"name": username, # Identité complète du compte telle que résolue par GPResult.
"domain": domain "account": rsop_user,
"username": username,
# Préfixe au format NetBIOS :
# nom de domaine pour un compte AD,
# nom du poste pour un compte local.
"account_domain": account_domain,
"account_type": account_type,
}, },
"denied_gpos": denied,
"applied_gpos": applied "summary": {
"applied_gpo_count": len(
applied_gpos
),
"denied_gpo_count": len(
denied_gpos
),
"total_gpo_count": (
len(applied_gpos)
+ len(denied_gpos)
),
"domain_group_count": len(
security_groups["domain"]
),
"local_group_count": len(
security_groups["local"]
),
"system_group_count": len(
security_groups["system"]
),
"other_group_count": len(
security_groups["other"]
),
},
# Des listes simples sont volontairement conservées
# afin de faciliter l'exploitation en SQL / reporting.
"applied_gpos": applied_gpos,
"denied_gpos": denied_gpos,
# Liste pratique contenant uniquement les groupes AD,
# généralement les plus intéressants dans un audit GPO.
"domain_groups":
security_groups["domain"],
# Le contexte complet est conservé pour faciliter le diagnostic.
"security_groups":
security_groups,
} }
_write_user_audit(data) WAPT.write_audit_data_if_changed(
AUDIT_SECTION,
AUDIT_NAME,
audit_data,
keep_days=365,
max_count=1
)
print( print(
"Audit disponible : audit-gpresult.user-gpo " "GPResult USER %s: "
"(%s appliquees, %s non appliquees)" "%s applied / %s denied"
% (len(applied), len(denied)) % (
rsop_user,
len(applied_gpos),
len(denied_gpos)
)
)
print(
"Account type: %s"
% account_type
)
print(
"AD groups: %s"
% len(
security_groups["domain"]
)
) )
return "OK" return "OK"
except Exception as e: 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) # Conserve également l'erreur dans les données d'audit WAPT
print("Erreur audit GPResult utilisateur : %s" % e) # afin qu'elle reste visible depuis la console.
WAPT.write_audit_data_if_changed(
AUDIT_SECTION,
AUDIT_NAME,
{
"status": "ERROR",
"message": str(e)
},
keep_days=365,
max_count=1
)
print(
"GPResult USER audit error: %s"
% str(e)
)
return "ERROR" return "ERROR"
finally:
# Le XML GPResult est uniquement utilisé comme fichier temporaire.
try:
if os.path.isfile(xml_file):
os.remove(xml_file)
except Exception:
pass