1er upload
This commit is contained in:
1 parent
a20caf3b51
commit
010f45b8cf
3 files changed
+375
-2
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user