Ajoute le script d'audit GPResult machine et sa documentation

This commit is contained in:
slaporte committed 2026-10-28 16:36:46 +01:00
1 parent fcd1a19cd9
commit 0fb81f2a9c
3 files changed
+385 -2

No files matched your search

+98 -2
View File
@@ -1,3 +1,99 @@
# Audit-gpresult-machine
**form-gpresult-computer**
Paquet WAPT d'audit des stratégies de groupe appliquées à l'ordinateur. Il collecte un export XML de GPResult et enregistre un résultat structuré dans **`audit-gpresult.computer-gpo`**.
Le nom `form-gpresult-computer` est proposé pour identifier ce paquet dans le dépôt, en complément de `form-gpresult-user`. Si le paquet machine existe déjà sous un autre nom, conserver son identité actuelle.
**Description pour le dépôt**
**Libellé proposé :** Audit GPO — Ordinateur
Ligne à reporter dans le champ `description` du fichier `WAPT/control` existant :
```
description: Audit des GPO machine : domaine, site, listes des GPO appliquees et non appliquees et compteurs dans audit-gpresult.computer-gpo.
 
```
Cette ligne est également fournie dans `WAPT/description.txt`. Ce fichier n'est pas un contrôle WAPT complet : conserver les autres métadonnées du paquet existant.
**Fonctionnement**
1. L'installation appelle `install()`, volontairement vide.
2. L'audit supprime, s'il existe, le rapport temporaire précédent.
3. Il lance GPResult depuis le chemin construit avec `makepath(system32(), "gpresult.exe")`, avec les options `/SCOPE COMPUTER /X "<rapport.xml>" /F` et un délai maximal de 120 secondes.
4. Il recherche la section XML `ComputerResults`, extrait les informations et classe les GPO.
5. Il enregistre directement un dictionnaire avec `WAPT.write_audit_data_if_changed`.
6. Il tente de supprimer le rapport temporaire dans un bloc `finally`.
Le paquet consulte les données RSoP disponibles. Il ne lance pas de `gpupdate` et ne modifie pas les stratégies de groupe. Il n'a pas besoin d'une session utilisateur active et ne collecte pas les GPO utilisateur.
**Prérequis**
- Poste Windows disposant de GPResult au chemin construit par les fonctions WAPT utilisées.
- Agent WAPT fournissant `setuphelpers` et `WAPT.write_audit_data_if_changed`.
- Données RSoP machine disponibles et droits suffisants dans le contexte de l'agent pour les consulter.
- Accès en lecture et écriture au répertoire temporaire du contexte d'exécution.
**Données produites**
**Catégorie :** `audit-gpresult` — **Clé :** `computer-gpo`.
Exemple illustratif :
```
{
   "status": "OK",
   "computer": {
     "name": "POSTE01",
     "domain": "exemple.lan",
     "site": "Default-First-Site-Name"
   },
   "summary": {
     "applied_gpo_count": 2,
     "denied_gpo_count": 1,
     "total_gpo_count": 3
   },
   "applied_gpos": [
     "Local Group Policy",
     "Default Domain Policy"
   ],
   "denied_gpos": [
     "GPO non appliquee"
   ]
 }
 
```
Audite les GPO appliquées/refusées sur la machine
| | |
| - | - |
| **Champ** | **Signification** |
| `status` | État de la collecte, pas un verdict de conformité. |
| `computer.name` | Nom du poste issu de `COMPUTERNAME`. |
| `computer.domain` | Premier texte `DomainName` trouvé dans le sous-arbre machine, sinon premier texte `Domain`. |
| `computer.site` | Premier texte `Site` trouvé dans le sous-arbre machine. |
| `summary.applied_gpo_count` | Nombre d'entrées retenues comme appliquées. |
| `summary.denied_gpo_count` | Nombre d'entrées retenues comme non appliquées. |
| `summary.total_gpo_count` | Somme des deux compteurs. |
| `applied_gpos` | Noms des GPO classées comme appliquées, dans l'ordre de collecte. |
| `denied_gpos` | Noms des GPO classées comme non appliquées, dans l'ordre de collecte. |
 
Les champs de domaine et de site restent des chaînes vides si aucune valeur n'est trouvée. La recherche de texte est récursive ; elle conserve le comportement du script fourni.
**Règles de classement conservées**
Une GPO est considérée comme appliquée si `Enabled`, `IsValid` et `FilterAllowed` sont vrais et si `AccessDenied` est faux. Toutes les autres entrées retenues sont placées dans `denied_gpos`.
Les valeurs `true`, `1`, `yes` et `oui`, sans distinction de casse, sont interprétées comme vraies. Toute autre valeur, y compris un champ absent, est interprétée comme fausse. Un `AccessDenied` absent est donc considéré comme faux, tandis qu'un `Enabled`, `IsValid` ou `FilterAllowed` absent empêche le classement en GPO appliquée.
**`denied_gpos` signifie ici « GPO non appliquées »**, et pas uniquement « refus de sécurité ». La liste peut notamment inclure des entrées filtrées, désactivées ou invalides.
La recherche des balises `GPO` est récursive. Les doublons sont éliminés selon le triplet `(nom, version annuaire, version SYSVOL)` ; un même nom peut donc apparaître plusieurs fois si les versions diffèrent. Les entrées sans nom sont ignorées. Les compteurs décrivent les entrées retenues par cet algorithme, pas un inventaire de toutes les GPO du domaine.
**Statuts**
| | |
| - | - |
| **Retour d'audit** | **Situation** |
| `OK` | Export, analyse et enregistrement terminés, même si des GPO non appliquées sont présentes. |
| `ERROR` | Échec de la commande, fichier absent, XML invalide, section machine absente ou autre exception capturée. |
 
En cas d'erreur, la même clé reçoit un objet contenant `status: ERROR` et `message`. Les champs du précédent succès ne sont pas recopiés. Les consommateurs doivent lire `status` avant d'accéder aux listes et compteurs.
Les paramètres d'enregistrement sont `keep_days=365` et `max_count=1`. Aucun statut `WARNING` n'est utilisé dans cette version. Une section `ComputerResults` présente mais sans GPO retenue produit un résultat `OK` avec des compteurs à zéro.
**Déploiement**
1. Copier le `setup.py` fourni dans les sources du paquet existant.
2. Reporter la description dans `WAPT/control` et incrémenter la version selon les conventions du dépôt.
3. Reconstruire, signer et téléverser le paquet.
4. Déployer la version, puis lancer l'audit depuis WAPT.
5. Consulter `audit-gpresult.computer-gpo`.
La fréquence des audits dépend de la configuration WAPT existante ; le script ne crée pas de planification.
**Dépannage et limites de cette version**
- Consulter le journal de l'audit pour connaître l'erreur GPResult ou XML.
- Vérifier l'accès à GPResult, les droits de collecte machine et les données RSoP.
- Le rapport temporaire utilise un nom fixe : `wapt-gpresult-computer.xml` dans `tempfile.gettempdir()`. Éviter des exécutions concurrentes de ce même audit dans le même répertoire temporaire.
- Le nettoyage est tenté dans tous les cas ; un échec de suppression est ignoré et peut laisser le rapport sur disque.
- La collecte restitue les données disponibles, sans forcer leur actualisation.
**Contenu**
- `setup.py` : code fourni, enrichi de commentaires et de docstrings, sans modification de logique.
- `README.md` : fonctionnement, interprétation, prérequis et exploitation.
- `WAPT/description.txt` : ligne de description pour le contrôle existant
+1
View File
@@ -0,0 +1 @@
description: Audit des GPO machine : domaine, site, listes des GPO appliquees et non appliquees et compteurs dans audit-gpresult.computer-gpo.
+286
View File
@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
"""Audit GPResult machine : resultat structure dans audit-gpresult.computer-gpo.
L'installation est vide. La collecte, l'analyse XML et l'enregistrement
des donnees sont realises uniquement lors de l'execution de audit().
"""
from setuphelpers import *
import os
import tempfile
import xml.etree.ElementTree as ET
# Categorie et cle des donnees d'audit WAPT.
AUDIT_SECTION = "audit-gpresult"
AUDIT_NAME = "computer-gpo"
def install():
"""Point d'entree d'installation ; aucune configuration a appliquer."""
pass
def _clean_tag(tag):
"""Retire l'espace de noms XML eventuel d'une balise."""
return tag.split("}")[-1]
def _text(element, tag, default=""):
"""Lit le premier texte non vide d'une balise dans tout le sous-arbre.
La recherche est recursive et insensible a la casse. Si aucune valeur
n'est trouvee, retourne la valeur par defaut.
"""
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 les valeurs reconnues en True ; toute autre valeur est False."""
return str(value).strip().lower() in (
"true",
"1",
"yes",
"oui"
)
def _find_computer_results(root):
"""Recherche la section machine sans dependre des espaces de noms XML."""
for element in root.iter():
if _clean_tag(element.tag) == "ComputerResults":
return element
return None
def _get_gpos(computer_results):
"""Classe les GPO machine en appliquees et non appliquees.
Les doublons sont elimines selon le triplet nom/version annuaire/
version SYSVOL. Les GPO sans nom sont ignorees.
"""
applied = []
denied = []
seen = set()
if computer_results is None:
return applied, denied
# Conserver la recherche recursive de la version de reference.
for element in computer_results.iter():
if _clean_tag(element.tag) != "GPO":
continue
name = _text(element, "Name")
if not name:
continue
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)
# Une GPO est appliquee uniquement si les trois indicateurs
# positifs sont vrais et si aucun refus d'acces n'est indique.
# denied regroupe toutes les autres entrees retenues.
if (
enabled
and valid
and filter_allowed
and not access_denied
):
applied.append(name)
else:
denied.append(name)
return applied, denied
def audit():
"""Exporte le GPResult machine, enregistre les donnees et retourne un statut."""
# Nom temporaire fixe conserve de la version fournie.
xml_file = os.path.join(
tempfile.gettempdir(),
"wapt-gpresult-computer.xml"
)
try:
# Eviter de lire le rapport d'une execution precedente.
if os.path.isfile(xml_file):
os.remove(xml_file)
# /SCOPE COMPUTER cible les GPO machine ; /X produit le XML ;
# /F autorise le remplacement du fichier de destination.
command = '"%s" /SCOPE COMPUTER /X "%s" /F' % (
makepath(system32(), "gpresult.exe"),
xml_file
)
print("Running GPResult COMPUTER...")
run(
command,
timeout=120,
return_stderr=False
)
if not os.path.isfile(xml_file):
raise Exception(
"GPResult did not generate XML report"
)
tree = ET.parse(xml_file)
root = tree.getroot()
computer_results = _find_computer_results(root)
if computer_results is None:
raise Exception(
"ComputerResults section not found"
)
applied_gpos, denied_gpos = _get_gpos(
computer_results
)
# Essayer DomainName, puis Domain si aucune valeur n'est trouvee.
domain = _text(
computer_results,
"DomainName"
)
if not domain:
domain = _text(
computer_results,
"Domain"
)
audit_data = {
"status": "OK",
"computer": {
"name": os.environ.get(
"COMPUTERNAME",
""
),
"domain": domain,
"site": _text(
computer_results,
"Site"
),
},
"summary": {
"applied_gpo_count": len(applied_gpos),
"denied_gpo_count": len(denied_gpos),
"total_gpo_count": (
len(applied_gpos)
+ len(denied_gpos)
),
},
"applied_gpos": applied_gpos,
"denied_gpos": denied_gpos,
}
# Transmettre le dictionnaire directement pour conserver une
# donnee structuree, plutot qu'une chaine de texte JSON.
WAPT.write_audit_data_if_changed(
AUDIT_SECTION,
AUDIT_NAME,
audit_data,
keep_days=365,
max_count=1
)
print(
"GPResult COMPUTER: %s applied / %s denied"
% (
len(applied_gpos),
len(denied_gpos)
)
)
return "OK"
except Exception as e:
# La meme cle est mise a jour avec l'erreur : les anciennes
# listes ne sont pas recopiees comme resultat de cet audit.
WAPT.write_audit_data_if_changed(
AUDIT_SECTION,
AUDIT_NAME,
{
"status": "ERROR",
"message": str(e)
},
keep_days=365,
max_count=1
)
print(
"GPResult COMPUTER audit error: %s"
% str(e)
)
return "ERROR"
finally:
# Nettoyage tente apres succes comme apres erreur.
# Un echec de suppression est ignore dans la version conservee.
try:
if os.path.isfile(xml_file):
os.remove(xml_file)
except:
pass