diff --git a/Deploy-WaptAgentGpo.ps1 b/Deploy-WaptAgentGpo.ps1
new file mode 100644
index 0000000..bbceab0
--- /dev/null
+++ b/Deploy-WaptAgentGpo.ps1
@@ -0,0 +1,696 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Crée (ou met à jour) une GPO qui déploie l'agent WAPT via une tâche planifiée.
+ Entièrement automatisé : seule l'adresse IP du serveur WAPT est demandée.
+
+.COMMANDE
+ .\Deploy-WaptAgentGpo.ps1 -WaptServerIP 10.0.0.10 -TargetOU "OU=Postes,DC=mydomain,DC=lan"
+
+.DESCRIPTION
+ Reproduit la méthode "GPO avec une tâche planifiée" de la documentation WAPT 2.6
+ (section 1.1.2.2), sans aucune question pendant l'exécution.
+
+ Informations récupérées automatiquement :
+ - domaine et contrôleur de domaine (PDC) : depuis l'Active Directory ;
+ - nom FQDN du serveur WAPT : depuis son certificat HTTPS, sinon le DNS inverse ;
+ - hash SHA256 de waptagent.exe : calculé sur le fichier du dossier du script,
+ puis comparé à la copie publiée par le serveur WAPT quand elle est accessible ;
+ - version minimale (--minversion) : version de fichier de waptagent.exe ;
+ - OU cible : le conteneur par défaut des ordinateurs du domaine s'il s'agit d'une OU
+ (redirection redircmp), sinon la GPO est créée sans liaison.
+
+ Étapes :
+ 1. contrôles préalables (modules, fichiers, droits d'écriture) ;
+ 2. serveur WAPT : joignabilité HTTPS, nom FQDN, résolution DNS ;
+ 3. domaine et contrôleur de domaine ;
+ 4. waptagent.exe : hash, version, comparaison avec le serveur ;
+ 5. waptdeploy.exe : fichier local, sinon téléchargé depuis le serveur (signature vérifiée) ;
+ 6. copie des deux exécutables dans NETLOGON ;
+ 7. GPO : préférences Fichiers et Tâche planifiée, extensions côté client, version ;
+ 8. liaison à l'OU.
+
+ Le module GroupPolicy ne propose aucune cmdlet pour les préférences Fichiers et
+ Tâches planifiées : le script écrit donc directement les fichiers XML dans SYSVOL,
+ au même format que la console GPMC.
+
+ Le script est rejouable. Relancez-le après chaque régénération de waptagent.exe :
+ il recopie le fichier dans NETLOGON et met à jour le hash dans la tâche planifiée.
+
+ Codes retour : 0 = succès, 2 = succès avec avertissements (GPO non liée, DNS du serveur
+ manquant, hash non comparé au serveur...), 1 = erreur. Un journal est écrit dans le
+ sous-dossier "logs" du dossier du script.
+
+.PARAMETER WaptServerIP
+ Adresse IPv4 du serveur WAPT. Seul paramètre obligatoire.
+
+.PARAMETER SourceFolder
+ Dossier contenant waptagent.exe (et waptdeploy.exe). Par défaut : dossier du script.
+
+.PARAMETER MinVersion
+ Force la version minimale de l'agent si elle ne peut pas être lue dans waptagent.exe.
+
+.PARAMETER TargetOU
+ Force l'OU de liaison (DN). Par défaut : conteneur des ordinateurs s'il s'agit d'une OU.
+
+.PARAMETER LocalFolder
+ Dossier de destination sur les postes. C:\Windows\SystemTemp (défaut, conforme à
+ la doc) existe sur Windows 11 et sur Windows 10 à partir de la 20H2.
+
+.EXAMPLE
+ .\Deploy-WaptAgentGpo.ps1 -WaptServerIP 10.0.0.10
+
+.EXAMPLE
+ .\Deploy-WaptAgentGpo.ps1 -WaptServerIP 10.0.0.10 -TargetOU "OU=Postes,DC=mydomain,DC=lan"
+
+.NOTES
+ Comitari - lab WAPT
+ Prérequis :
+ - exécution sur un contrôleur de domaine ou un poste du domaine avec RSAT
+ (modules GroupPolicy et ActiveDirectory) ;
+ - compte autorisé à créer des GPO (et à les lier sur l'OU cible) ;
+ - waptagent.exe (généré depuis la console WAPT) dans le dossier du script ;
+ waptdeploy.exe dans le même dossier, ou téléchargeable depuis le serveur WAPT.
+ Source : https://www.wapt.fr/fr/doc-2.6/wapt-deploy-agent.html#gpo-with-a-scheduled-task
+#>
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true, Position = 0)]
+ [ValidateScript({
+ $ip = $null
+ if ([System.Net.IPAddress]::TryParse($_, [ref]$ip) -and $ip.AddressFamily -eq 'InterNetwork') { $true }
+ else { throw "'$_' n'est pas une adresse IPv4 valide." }
+ })]
+ [string]$WaptServerIP,
+
+ [string]$SourceFolder = $PSScriptRoot,
+
+ [string]$MinVersion,
+
+ [string]$TargetOU,
+
+ [string]$GpoName = 'WAPT-Deploiement-Agent',
+
+ [string]$TaskName = 'WAPT - waptdeploy',
+
+ [string]$LocalFolder = 'C:\Windows\SystemTemp',
+
+ [ValidateRange(1, 120)]
+ [int]$WaitMinutes = 15
+)
+
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue' # accélère fortement Invoke-WebRequest sous PowerShell 5.1
+
+# ---------------------------------------------------------------------------
+# Constantes
+# ---------------------------------------------------------------------------
+# Group Policy Preferences (identifiants fixes définis par Microsoft)
+$CSE_CORE = '{00000000-0000-0000-0000-000000000000}'
+$CSE_FILES = '{7150F9BF-48AD-4DA4-A49C-29EF4A8369BA}'
+$TOOL_FILES = '{3BAE7E51-E3F4-41D0-853D-9BB9FD47605F}'
+$CSE_SCHEDTASKS = '{AADCED64-746C-4633-A97C-D61349046527}'
+$TOOL_SCHEDTASKS = '{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}'
+
+$VERSION_PATTERN = '^\d+(\.\d+){1,3}$'
+$script:WarningCount = 0
+$script:CertError = $null
+$TOTAL_STEPS = 8
+
+# Emplacements publiés par le serveur WAPT (doc 2.6)
+$AGENT_URL_PATHS = @('/wapt/waptagent.exe', '/wapt/waptagent/waptagent.exe')
+$DEPLOY_URL_PATHS = @('/wapt/waptagent/waptdeploy.exe', '/wapt/waptdeploy.exe')
+
+# ---------------------------------------------------------------------------
+# Fonctions utilitaires : affichage, chemins, XML
+# ---------------------------------------------------------------------------
+function Write-Step([int]$Number, [string]$Text) {
+ Write-Host ''
+ Write-Host "[$Number/$TOTAL_STEPS] $Text" -ForegroundColor Cyan
+}
+
+function Write-Ok([string]$Text) { Write-Host " OK $Text" -ForegroundColor Green }
+function Write-Info([string]$Text) { Write-Host " ... $Text" }
+function Write-Warn([string]$Text) {
+ $script:WarningCount++
+ Write-Host " /!\ $Text" -ForegroundColor Yellow
+}
+
+function New-GppUid {
+ '{' + [guid]::NewGuid().ToString().ToUpper() + '}'
+}
+
+function Join-LocalPath([string]$Folder, [string]$File) {
+ # Chemin Windows côté poste client : simple concaténation, sans vérifier le lecteur
+ $Folder.TrimEnd('\') + '\' + $File
+}
+
+function ConvertTo-XmlText([string]$Text) {
+ [System.Security.SecurityElement]::Escape($Text)
+}
+
+function Write-Utf8File([string]$Path, [string]$Content) {
+ $full = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
+ $dir = Split-Path -Parent $full
+ if (-not (Test-Path -LiteralPath $dir)) {
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
+ }
+ [System.IO.File]::WriteAllText($full, $Content, (New-Object System.Text.UTF8Encoding($false)))
+}
+
+function New-FilesXml {
+ param([string]$NetlogonUnc, [string]$LocalFolder)
+
+ $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+ $deploySrc = ConvertTo-XmlText "$NetlogonUnc\waptdeploy.exe"
+ $agentSrc = ConvertTo-XmlText "$NetlogonUnc\waptagent.exe"
+ $deployDst = ConvertTo-XmlText (Join-LocalPath $LocalFolder 'waptdeploy.exe')
+ $agentDst = ConvertTo-XmlText (Join-LocalPath $LocalFolder 'waptagent.exe')
+
+ # action R = Remplacer, U = Mettre à jour (image = icône associée dans GPMC)
+ @"
+
+
+
+
+
+
+
+
+
+"@
+}
+
+function New-ScheduledTasksXml {
+ param([string]$TaskName, [string]$Command, [string]$Arguments, [string]$Author)
+
+ $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+ $start = Get-Date -Format "yyyy-MM-dd'T'HH:mm:00"
+ $name = ConvertTo-XmlText $TaskName
+ $cmd = ConvertTo-XmlText $Command
+ $args_ = ConvertTo-XmlText $Arguments
+ $auth = ConvertTo-XmlText $Author
+
+ # Onglet Général : compte S-1-5-18, exécuter même si l'utilisateur n'est pas connecté,
+ # privilèges les plus élevés.
+ # Onglet Déclencheurs : quotidien, répéter toutes les heures pendant 1 jour,
+ # arrêter si la tâche dure plus de 2 heures, activé.
+ # Onglet Paramètres : seule "Exécuter la tâche dès que possible si un démarrage
+ # planifié est manqué" est cochée.
+ @"
+
+
+
+
+
+
+ $auth
+ Deploiement de l'agent WAPT via waptdeploy.exe (GPO tache planifiee)
+
+
+
+ S-1-5-18
+ S4U
+ HighestAvailable
+
+
+
+
+ PT10M
+ PT1H
+ false
+ false
+
+ IgnoreNew
+ false
+ false
+ false
+ true
+ false
+ true
+ false
+ PT0S
+ 7
+
+
+
+
+ PT1H
+ P1D
+ false
+
+ $start
+ PT2H
+ true
+
+ 1
+
+
+
+
+
+ $cmd
+ $args_
+
+
+
+
+
+
+"@
+}
+
+function Merge-ExtensionNames {
+ # Fusionne les extensions existantes de la GPO avec celles à ajouter.
+ # Format AD : [{CSE}{Outil}{Outil}][{CSE}{Outil}]..., trié par GUID.
+ param([string]$Existing, [hashtable]$ToAdd)
+
+ $map = @{}
+ if ($Existing) {
+ foreach ($block in [regex]::Matches($Existing, '\[([^\]]+)\]')) {
+ $guids = @([regex]::Matches($block.Groups[1].Value, '\{[0-9A-Fa-f\-]{36}\}') |
+ ForEach-Object { $_.Value.ToUpper() })
+ if ($guids.Count -eq 0) { continue }
+ if (-not $map.ContainsKey($guids[0])) { $map[$guids[0]] = @() }
+ $map[$guids[0]] += @($guids | Select-Object -Skip 1)
+ }
+ }
+ foreach ($cse in $ToAdd.Keys) {
+ $key = $cse.ToUpper()
+ if (-not $map.ContainsKey($key)) { $map[$key] = @() }
+ $map[$key] += @($ToAdd[$cse] | ForEach-Object { $_.ToUpper() })
+ }
+
+ $result = ''
+ foreach ($cse in ($map.Keys | Sort-Object)) {
+ $tools = @($map[$cse] | Sort-Object -Unique)
+ $result += '[' + $cse + ($tools -join '') + ']'
+ }
+ $result
+}
+
+# ---------------------------------------------------------------------------
+# Fonctions utilitaires : réseau et fichiers WAPT
+# ---------------------------------------------------------------------------
+function Test-TcpPort([string]$HostName, [int]$Port, [int]$TimeoutMs = 5000) {
+ $tcp = New-Object System.Net.Sockets.TcpClient
+ try {
+ $tcp.ConnectAsync($HostName, $Port).Wait($TimeoutMs) -and $tcp.Connected
+ } catch {
+ $false
+ } finally {
+ $tcp.Dispose()
+ }
+}
+
+function Get-ServerCertificateName([string]$Ip, [int]$Port = 443) {
+ # Lit le nom DNS du certificat HTTPS du serveur WAPT (FQDN saisi lors du postconf).
+ # Le certificat n'est pas validé ici : on lit seulement le nom qu'il contient.
+ $tcp = New-Object System.Net.Sockets.TcpClient
+ try {
+ if (-not $tcp.ConnectAsync($Ip, $Port).Wait(5000)) { return $null }
+ $callback = [System.Net.Security.RemoteCertificateValidationCallback] { $true }
+ $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, $callback)
+ try {
+ # TLS 1.2 explicite : sous PowerShell 5.1, la valeur par défaut (SSL3/TLS 1.0)
+ # est refusée par le nginx du serveur WAPT.
+ $ssl.AuthenticateAsClient($Ip, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false)
+ $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($ssl.RemoteCertificate)
+ $name = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::DnsName, $false)
+ if ($name -and $name -ne $Ip) { return $name.TrimEnd('.').ToLower() }
+ } finally {
+ $ssl.Dispose()
+ }
+ } catch {
+ $script:CertError = $_.Exception.GetBaseException().Message
+ } finally {
+ $tcp.Dispose()
+ }
+ $null
+}
+
+function Get-ReverseDnsName([string]$Ip) {
+ $ptr = $null
+ try {
+ $ptr = Resolve-DnsName -Name $Ip -Type PTR -DnsOnly -ErrorAction SilentlyContinue |
+ Where-Object { $_.Type -eq 'PTR' } | Select-Object -First 1
+ } catch { }
+ if ($ptr) { return $ptr.NameHost.TrimEnd('.').ToLower() }
+ $null
+}
+
+function Test-ForwardDns([string]$Fqdn, [string]$Ip) {
+ $ips = @()
+ try {
+ $ips = @(Resolve-DnsName -Name $Fqdn -Type A -DnsOnly -ErrorAction SilentlyContinue |
+ Where-Object { $_.Type -eq 'A' } | ForEach-Object { $_.IPAddress })
+ } catch { }
+ $ips -contains $Ip
+}
+
+function Set-RelaxedTls([bool]$Enabled) {
+ # PowerShell 5.1 : accepte temporairement le certificat auto-signé du serveur WAPT.
+ # (PowerShell 7 utilise -SkipCertificateCheck, voir Invoke-WaptDownload.)
+ if ($PSVersionTable.PSVersion.Major -ge 6) { return }
+ if (-not ('WaptRelaxedTls' -as [type])) {
+ Add-Type -TypeDefinition @"
+using System.Net;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+public static class WaptRelaxedTls {
+ private static bool Accept(object s, X509Certificate c, X509Chain ch, SslPolicyErrors e) { return true; }
+ public static void Enable() { ServicePointManager.ServerCertificateValidationCallback = Accept; }
+ public static void Disable() { ServicePointManager.ServerCertificateValidationCallback = null; }
+}
+"@
+ }
+ if ($Enabled) { [WaptRelaxedTls]::Enable() } else { [WaptRelaxedTls]::Disable() }
+}
+
+function Invoke-WaptDownload {
+ # Télécharge le premier fichier disponible parmi les URL proposées.
+ # Essai avec validation du certificat, puis sans (certificat auto-signé du postconf).
+ param([string]$BaseUrl, [string[]]$Paths, [string]$OutFile)
+
+ foreach ($path in $Paths) {
+ $url = $BaseUrl + $path
+ foreach ($relaxed in @($false, $true)) {
+ try {
+ $params = @{ Uri = $url; OutFile = $OutFile; UseBasicParsing = $true; TimeoutSec = 300 }
+ if ($relaxed) {
+ if ($PSVersionTable.PSVersion.Major -ge 6) { $params.SkipCertificateCheck = $true }
+ else { Set-RelaxedTls $true }
+ }
+ Invoke-WebRequest @params
+ return [pscustomobject]@{ Url = $url; Relaxed = $relaxed }
+ } catch {
+ Write-Verbose "Échec $url (certificat ignoré : $relaxed) : $($_.Exception.Message)"
+ } finally {
+ if ($relaxed) { Set-RelaxedTls $false }
+ }
+ }
+ }
+ $null
+}
+
+function Get-AgentVersion([string]$Path) {
+ $info = (Get-Item -LiteralPath $Path).VersionInfo
+ foreach ($candidate in @($info.FileVersion, $info.ProductVersion)) {
+ if (-not $candidate) { continue }
+ $v = ($candidate -replace ',', '.') -replace '\s', ''
+ if ($v -match $VERSION_PATTERN -and $v -notmatch '^0(\.0)*$') { return $v }
+ }
+ $null
+}
+
+function Test-TranquilSignature([string]$Path) {
+ $sig = Get-AuthenticodeSignature -LiteralPath $Path
+ $subject = if ($sig.SignerCertificate) { $sig.SignerCertificate.Subject } else { '(aucun)' }
+ [pscustomobject]@{
+ Valid = ($sig.Status -eq 'Valid' -and $subject -match 'Tranquil')
+ Status = $sig.Status
+ Signer = $subject
+ }
+}
+
+# ---------------------------------------------------------------------------
+# Déroulé principal
+# ---------------------------------------------------------------------------
+function Invoke-Deployment {
+
+ # 1. Contrôles préalables -----------------------------------------------------------
+ Write-Step 1 'Contrôles préalables'
+
+ foreach ($module in 'GroupPolicy', 'ActiveDirectory') {
+ if (-not (Get-Module -ListAvailable -Name $module)) {
+ throw "Module $module absent. Lancez le script sur un DC ou installez les outils RSAT."
+ }
+ Import-Module $module
+ }
+ Write-Ok 'Modules GroupPolicy et ActiveDirectory chargés'
+
+ $agentSource = Join-Path $SourceFolder 'waptagent.exe'
+ if (-not (Test-Path -LiteralPath $agentSource -PathType Leaf)) {
+ throw "waptagent.exe introuvable dans $SourceFolder. Générez l'agent depuis la console WAPT et copiez-le à côté du script."
+ }
+ Write-Ok "waptagent.exe présent dans $SourceFolder"
+
+ $setupFiles = @(Get-ChildItem -LiteralPath $SourceFolder -Filter 'waptsetup*.exe' -ErrorAction SilentlyContinue)
+ if ($setupFiles) {
+ Write-Info "$($setupFiles[0].Name) présent : il installe la console et n'est pas utilisé par cette GPO."
+ }
+
+ if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
+ }
+
+ # 2. Serveur WAPT ----------------------------------------------------------------------
+ Write-Step 2 "Serveur WAPT $WaptServerIP"
+
+ if (-not (Test-TcpPort -HostName $WaptServerIP -Port 443)) {
+ throw "Le serveur $WaptServerIP ne répond pas sur le port 443. Vérifiez l'IP, le service nginx et le pare-feu."
+ }
+ Write-Ok 'Port 443 joignable'
+
+ $fqdn = Get-ServerCertificateName -Ip $WaptServerIP
+ $fqdnSource = 'certificat HTTPS'
+ if (-not $fqdn) {
+ $fqdn = Get-ReverseDnsName -Ip $WaptServerIP
+ $fqdnSource = 'DNS inverse'
+ }
+
+ $downloadHost = $WaptServerIP
+ if ($fqdn) {
+ Write-Ok "Nom du serveur : $fqdn (source : $fqdnSource)"
+ if (Test-ForwardDns -Fqdn $fqdn -Ip $WaptServerIP) {
+ Write-Ok "$fqdn est résolu vers $WaptServerIP par le DNS du domaine"
+ $downloadHost = $fqdn
+ } else {
+ Write-Warn "$fqdn n'est pas résolu vers $WaptServerIP par le DNS du domaine."
+ Write-Warn "Les agents contactent le serveur par ce nom : créez l'enregistrement A avant de déployer."
+ }
+ } else {
+ Write-Warn 'Nom du serveur introuvable (ni certificat lisible, ni DNS inverse).'
+ if ($script:CertError) { Write-Warn "Lecture du certificat : $($script:CertError)" }
+ Write-Warn 'Vérifiez que le nom du serveur saisi lors de la génération de l''agent est résolu par le DNS du domaine.'
+ }
+ $baseUrl = "https://$downloadHost"
+
+ # 3. Domaine et contrôleur de domaine ----------------------------------------------------
+ Write-Step 3 'Domaine et contrôleur de domaine'
+
+ $domain = Get-ADDomain
+ $dnsRoot = $domain.DNSRoot
+ $dc = $domain.PDCEmulator # un seul DC pour toutes les écritures : pas d'attente de réplication
+ Write-Ok "Domaine : $dnsRoot"
+ Write-Ok "Contrôleur utilisé : $dc (PDC)"
+
+ # 4. waptagent.exe : hash, version, comparaison avec le serveur ----------------------------
+ Write-Step 4 'waptagent.exe'
+
+ $agentHash = (Get-FileHash -LiteralPath $agentSource -Algorithm SHA256).Hash.ToLower()
+ Write-Ok "SHA256 : $agentHash"
+
+ if (-not $MinVersion) {
+ $MinVersion = Get-AgentVersion -Path $agentSource
+ if (-not $MinVersion) {
+ throw 'Version illisible dans waptagent.exe. Relancez avec -MinVersion (ex. -MinVersion 2.6.1.17654).'
+ }
+ Write-Ok "Version minimale (lue dans waptagent.exe) : $MinVersion"
+ } else {
+ if ($MinVersion -notmatch $VERSION_PATTERN) { throw "Version invalide : $MinVersion" }
+ Write-Ok "Version minimale (forcée) : $MinVersion"
+ }
+
+ $serverCopy = Join-Path ([System.IO.Path]::GetTempPath()) ("waptagent_server_{0}.exe" -f [guid]::NewGuid().ToString('N'))
+ try {
+ Write-Info "Téléchargement de la copie publiée par le serveur pour comparaison..."
+ $dl = Invoke-WaptDownload -BaseUrl $baseUrl -Paths $AGENT_URL_PATHS -OutFile $serverCopy
+ if ($dl) {
+ if ($dl.Relaxed) { Write-Info 'Certificat du serveur non approuvé (normal avec le certificat auto-signé du postconf).' }
+ $serverHash = (Get-FileHash -LiteralPath $serverCopy -Algorithm SHA256).Hash.ToLower()
+ if ($serverHash -ne $agentHash) {
+ throw ("Le waptagent.exe du dossier ne correspond pas à celui publié par le serveur ($($dl.Url)).`n" +
+ " Local : $agentHash`n Serveur : $serverHash`n" +
+ "Copiez la dernière version générée de waptagent.exe à côté du script, puis relancez.")
+ }
+ Write-Ok "Identique au fichier publié par le serveur ($($dl.Url))"
+ } else {
+ Write-Warn 'Copie du serveur non téléchargeable : hash calculé sur le fichier local uniquement.'
+ Write-Warn 'Assurez-vous que waptagent.exe est bien la dernière version générée depuis la console.'
+ }
+ } finally {
+ Remove-Item -LiteralPath $serverCopy -Force -ErrorAction SilentlyContinue
+ }
+
+ # 5. waptdeploy.exe -------------------------------------------------------------------------
+ Write-Step 5 'waptdeploy.exe'
+
+ $deploySource = Join-Path $SourceFolder 'waptdeploy.exe'
+ if (Test-Path -LiteralPath $deploySource -PathType Leaf) {
+ Write-Ok "Fichier local utilisé : $deploySource"
+ $sig = Test-TranquilSignature -Path $deploySource
+ if ($sig.Valid) { Write-Ok "Signature valide : $($sig.Signer)" }
+ else { Write-Warn "Signature non vérifiée (statut : $($sig.Status), signataire : $($sig.Signer))." }
+ } else {
+ Write-Info 'Absent du dossier : téléchargement depuis le serveur WAPT...'
+ $dl = Invoke-WaptDownload -BaseUrl $baseUrl -Paths $DEPLOY_URL_PATHS -OutFile $deploySource
+ if (-not $dl) {
+ throw "waptdeploy.exe absent du dossier et non téléchargeable depuis $baseUrl. Récupérez-le sur la page d'accueil du serveur WAPT."
+ }
+ $sig = Test-TranquilSignature -Path $deploySource
+ if (-not $sig.Valid) {
+ Remove-Item -LiteralPath $deploySource -Force -ErrorAction SilentlyContinue
+ throw "waptdeploy.exe téléchargé rejeté : signature Authenticode Tranquil IT non valide (statut : $($sig.Status), signataire : $($sig.Signer))."
+ }
+ Write-Ok "Téléchargé depuis $($dl.Url), signature valide : $($sig.Signer)"
+ }
+
+ # 6. Copie dans NETLOGON ----------------------------------------------------------------------
+ Write-Step 6 'Copie dans NETLOGON'
+
+ $netlogonDc = "\\$dc\NETLOGON" # écriture sur le PDC
+ $netlogonUnc = "\\$dnsRoot\NETLOGON" # chemin lu par les postes (comme dans la doc)
+
+ Copy-Item -LiteralPath $deploySource -Destination (Join-Path $netlogonDc 'waptdeploy.exe') -Force
+ Copy-Item -LiteralPath $agentSource -Destination (Join-Path $netlogonDc 'waptagent.exe') -Force
+
+ $copiedHash = (Get-FileHash -LiteralPath (Join-Path $netlogonDc 'waptagent.exe') -Algorithm SHA256).Hash.ToLower()
+ if ($copiedHash -ne $agentHash) { throw 'La copie de waptagent.exe dans NETLOGON est corrompue (hash différent).' }
+ Write-Ok "waptdeploy.exe et waptagent.exe copiés dans $netlogonDc (hash vérifié)"
+
+ # 7. GPO ----------------------------------------------------------------------------------------
+ Write-Step 7 "GPO $GpoName"
+
+ $gpo = Get-GPO -Name $GpoName -Server $dc -ErrorAction SilentlyContinue
+ if ($gpo) {
+ Write-Ok 'GPO existante : mise à jour'
+ } else {
+ $gpo = New-GPO -Name $GpoName -Server $dc -Comment 'Deploiement agent WAPT par tache planifiee (doc WAPT 2.6, 1.1.2.2)'
+ Write-Ok 'GPO créée'
+ }
+
+ $gpoId = '{' + $gpo.Id.ToString().ToUpper() + '}'
+ $gpoPath = "\\$dc\SYSVOL\$dnsRoot\Policies\$gpoId"
+
+ # Paramètres ordinateur uniquement : on désactive la partie utilisateur
+ $gpo.GpoStatus = 'UserSettingsDisabled'
+
+ $command = Join-LocalPath $LocalFolder 'waptdeploy.exe'
+ $arguments = "--hash=$agentHash --minversion=$MinVersion --wait=$WaitMinutes --waptsetupurl=$(Join-LocalPath $LocalFolder 'waptagent.exe')"
+
+ Write-Utf8File -Path "$gpoPath\Machine\Preferences\Files\Files.xml" `
+ -Content (New-FilesXml -NetlogonUnc $netlogonUnc -LocalFolder $LocalFolder)
+ Write-Utf8File -Path "$gpoPath\Machine\Preferences\ScheduledTasks\ScheduledTasks.xml" `
+ -Content (New-ScheduledTasksXml -TaskName $TaskName -Command $command -Arguments $arguments `
+ -Author "$env:USERDOMAIN\$env:USERNAME")
+ Write-Ok 'Préférences Fichiers et Tâche planifiée écrites'
+
+ $adGpo = Get-ADObject -Server $dc `
+ -Identity "CN=$gpoId,CN=Policies,CN=System,$($domain.DistinguishedName)" `
+ -Properties versionNumber, gPCMachineExtensionNames
+
+ $extensions = Merge-ExtensionNames -Existing $adGpo.gPCMachineExtensionNames -ToAdd @{
+ $CSE_CORE = @($TOOL_FILES, $TOOL_SCHEDTASKS)
+ $CSE_FILES = @($TOOL_FILES)
+ $CSE_SCHEDTASKS = @($TOOL_SCHEDTASKS)
+ }
+
+ # versionNumber : 16 bits de poids fort = utilisateur, 16 bits de poids faible = ordinateur
+ $version = [int]$adGpo.versionNumber
+ $newVersion = (($version -shr 16) -shl 16) + (($version -band 0xFFFF) + 1)
+
+ Set-ADObject -Server $dc -Identity $adGpo -Replace @{
+ gPCMachineExtensionNames = $extensions
+ versionNumber = $newVersion
+ }
+
+ $gptIni = Join-Path $gpoPath 'GPT.INI'
+ $ini = @(Get-Content -LiteralPath $gptIni)
+ if ($ini -match '^Version=') {
+ $ini = $ini -replace '^Version=\d+', "Version=$newVersion"
+ } else {
+ $ini += "Version=$newVersion"
+ }
+ Set-Content -LiteralPath $gptIni -Value $ini -Encoding ASCII
+ Write-Ok "Extensions déclarées, version $version -> $newVersion"
+
+ # 8. Liaison à l'OU --------------------------------------------------------------------------------
+ Write-Step 8 'Liaison'
+
+ $ou = $TargetOU
+ if (-not $ou) {
+ if ($domain.ComputersContainer -match '^OU=') {
+ $ou = $domain.ComputersContainer
+ Write-Info "OU par défaut des ordinateurs du domaine : $ou"
+ }
+ }
+
+ $linkedTo = $null
+ if (-not $ou) {
+ Write-Warn "Le conteneur des ordinateurs ($($domain.ComputersContainer)) n'est pas une OU : une GPO ne peut pas y être liée."
+ Write-Warn 'GPO créée sans liaison. Relancez avec -TargetOU "OU=...", ou liez-la dans GPMC.'
+ } else {
+ Get-ADOrganizationalUnit -Identity $ou -Server $dc | Out-Null # échoue si l'OU n'existe pas
+ $already = (Get-GPInheritance -Target $ou -Server $dc).GpoLinks | Where-Object { $_.GpoId -eq $gpo.Id }
+ if ($already) {
+ Write-Ok "Déjà liée à $ou"
+ $linkedTo = $ou
+ } else {
+ try {
+ New-GPLink -Guid $gpo.Id -Target $ou -LinkEnabled Yes -Server $dc | Out-Null
+ Write-Ok "Liée à $ou"
+ $linkedTo = $ou
+ } catch {
+ Write-Warn "Liaison refusée sur $ou : $($_.Exception.Message)"
+ Write-Warn 'Le compte peut créer des GPO mais pas les lier sur cette OU (droit gPLink). Liez-la dans GPMC.'
+ }
+ }
+ }
+
+ # Récapitulatif --------------------------------------------------------------------------------------
+ Write-Host ''
+ Write-Host '=== Récapitulatif ===' -ForegroundColor Cyan
+ Write-Host "Serveur WAPT : $WaptServerIP$(if ($fqdn) { " ($fqdn)" })"
+ Write-Host "Domaine / DC : $dnsRoot / $dc"
+ Write-Host "GPO : $GpoName $gpoId"
+ Write-Host "Liaison : $(if ($linkedTo) { $linkedTo } else { '(aucune)' })"
+ Write-Host "Tâche : $TaskName"
+ Write-Host "Commande : $command $arguments"
+ Write-Host ''
+ Write-Host 'Vérification sur un poste de l''OU (PowerShell administrateur) :' -ForegroundColor Cyan
+ Write-Host ' gpupdate /force'
+ Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Format-List TaskName, State, Principal"
+ Write-Host " Start-ScheduledTask -TaskName '$TaskName'"
+}
+
+# ---------------------------------------------------------------------------
+# Point d'entrée (ignoré si le script est chargé en dot-sourcing pour des tests)
+# ---------------------------------------------------------------------------
+if ($MyInvocation.InvocationName -eq '.') { return }
+
+$logDir = Join-Path $SourceFolder 'logs'
+$logFile = Join-Path $logDir ("Deploy-WaptAgentGpo_{0}.log" -f (Get-Date -Format 'yyyyMMdd_HHmmss'))
+New-Item -ItemType Directory -Path $logDir -Force | Out-Null
+Start-Transcript -LiteralPath $logFile | Out-Null
+
+$exitCode = 0
+try {
+ Invoke-Deployment
+ Write-Host ''
+ if ($script:WarningCount -gt 0) {
+ $exitCode = 2
+ Write-Host "Terminé avec $($script:WarningCount) avertissement(s) : relisez les lignes /!\ ci-dessus." -ForegroundColor Yellow
+ } else {
+ Write-Host 'Terminé avec succès.' -ForegroundColor Green
+ }
+} catch {
+ $exitCode = 1
+ Write-Host ''
+ Write-Host "ÉCHEC : $($_.Exception.Message)" -ForegroundColor Red
+ Write-Host "Ligne $($_.InvocationInfo.ScriptLineNumber) : $($_.InvocationInfo.Line.Trim())" -ForegroundColor DarkGray
+} finally {
+ Write-Host "Journal : $logFile"
+ Stop-Transcript | Out-Null
+}
+exit $exitCode
diff --git a/README.md b/README.md
index c55e3bb..39b3998 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,92 @@
# comi-deploy-waptagentgpo
-créé une gpo de tache planifié pour pousser l'installation de l'agent wapt
\ No newline at end of file
+Crée ou met à jour une GPO de déploiement de l'agent WAPT par tâche planifiée.
+
+## Avant de lancer le script
+
+Sur un contrôleur de domaine ou un poste du domaine équipé des outils RSAT :
+
+1. Ouvrir **Windows PowerShell 5.1 avec élévation**, avec un compte disposant des droits de création/modification des GPO et d'écriture dans NETLOGON et SYSVOL. Pour la liaison, le compte doit aussi pouvoir lier une GPO à l'OU cible.
+2. Placer **`waptagent.exe`, généré depuis la console WAPT**, dans le même dossier que `Deploy-WaptAgentGpo.ps1`. Un fichier `waptsetup*.exe` ne le remplace pas.
+3. Placer également `waptdeploy.exe` dans ce dossier, ou laisser le script le télécharger depuis le serveur WAPT.
+4. Se placer dans le dossier du script. Les modules **`GroupPolicy`** et **`ActiveDirectory`** doivent être disponibles et le serveur WAPT accessible en HTTPS sur le port 443.
+
+Dans les commandes suivantes, remplacer `10.0.0.10` par l'adresse IPv4 du serveur WAPT et le DN de l'OU par celui de votre domaine.
+
+## Possibilité 1 — Créer la GPO et la lier à une OU
+
+**Commande à lancer :**
+
+```powershell
+.\Deploy-WaptAgentGpo.ps1 -WaptServerIP 10.0.0.10 -TargetOU "OU=Postes,DC=mydomain,DC=lan"
+```
+
+Le script crée ou met à jour la GPO **`WAPT-Deploiement-Agent`**, puis crée un lien activé sur l'OU indiquée si ce lien n'existe pas déjà. L'OU doit exister et contenir les ordinateurs à cibler, directement ou par héritage.
+
+Si la création du lien est refusée, la GPO reste créée ou mise à jour ; le script signale un avertissement. La liaison peut alors être effectuée dans la console **Gestion de stratégie de groupe (GPMC)** avec un compte disposant des droits nécessaires.
+
+## Possibilité 2 — Créer la GPO sans préciser d'OU
+
+**Commande à lancer :**
+
+```powershell
+.\Deploy-WaptAgentGpo.ps1 -WaptServerIP 10.0.0.10
+```
+
+**Cette commande crée la GPO sans liaison si le conteneur par défaut des ordinateurs est un conteneur tel que `CN=Computers`.** La GPO peut ensuite être liée manuellement à l'OU souhaitée dans GPMC : clic droit sur l'OU, puis **Lier un objet de stratégie de groupe existant**, et sélectionner `WAPT-Deploiement-Agent`.
+
+**Attention : si le conteneur par défaut des ordinateurs a été redirigé vers une OU, le script lie automatiquement la GPO à cette OU, même sans `-TargetOU`.** La version actuelle ne propose pas de paramètre pour forcer une création sans liaison. Elle ne supprime pas non plus les liens d'une GPO existante.
+
+Pour connaître le conteneur par défaut avant de choisir cette commande :
+
+```powershell
+(Get-ADDomain).ComputersContainer
+```
+
+Vérifier la ligne **`Liaison`** du récapitulatif final : `(aucune)` indique qu'aucune liaison n'a été effectuée ou reconnue par cette exécution. Cela ne constitue pas un inventaire des liens préexistants sur d'autres OU.
+
+## Ce que configure le script
+
+- Détecte le domaine et utilise son contrôleur PDC pour les écritures.
+- Recherche le nom du serveur WAPT dans son certificat HTTPS, puis dans le DNS inverse, et vérifie sa résolution DNS.
+- Calcule le SHA256 et lit la version de `waptagent.exe`. Il compare le hash avec la copie du serveur lorsqu'elle est téléchargeable ; une différence bloque l'exécution.
+- Utilise le `waptdeploy.exe` local ou le télécharge. Un téléchargement est rejeté si sa signature Authenticode Tranquil IT n'est pas valide ; une signature locale non vérifiée produit un avertissement.
+- Copie les deux exécutables dans NETLOGON et configure leur copie sur les postes vers **`C:\Windows\SystemTemp`** par défaut.
+- Écrit les préférences Fichiers et Tâche planifiée de la GPO dans SYSVOL, met à jour les extensions et la version de la GPO, et désactive sa partie utilisateur.
+- Configure la tâche **`WAPT - waptdeploy`** sous **SYSTEM**, avec les privilèges les plus élevés, un déclenchement quotidien et une répétition toutes les heures pendant une journée.
+
+La tâche appelle `waptdeploy.exe` avec le hash de l'agent, sa version minimale, un délai `--wait` de 15 minutes par défaut et le chemin local de `waptagent.exe`.
+
+## Paramètres utiles
+
+| Paramètre | Utilisation |
+| --- | --- |
+| `-WaptServerIP` | Adresse IPv4 du serveur WAPT ; obligatoire. |
+| `-TargetOU` | DN de l'OU à laquelle lier la GPO ; comportement automatique si absent, décrit ci-dessus. |
+| `-SourceFolder` | Dossier des exécutables ; dossier du script par défaut. Les journaux y sont également créés. |
+| `-MinVersion` | Version minimale à imposer ; lue dans `waptagent.exe` par défaut. À préciser si elle ne peut pas être lue. |
+| `-GpoName` | Nom de la GPO ; `WAPT-Deploiement-Agent` par défaut. |
+| `-TaskName` | Nom de la tâche ; `WAPT - waptdeploy` par défaut. |
+| `-LocalFolder` | Dossier de destination sur les postes ; `C:\Windows\SystemTemp` par défaut. Vérifier sa présence sur les postes ciblés. |
+| `-WaitMinutes` | Valeur transmise à `--wait` ; de 1 à 120, avec 15 par défaut. |
+
+## Journaux et vérification
+
+Le journal est enregistré dans **`logs\Deploy-WaptAgentGpo_yyyyMMdd_HHmmss.log`**, sous le dossier des exécutables.
+
+| Code retour | Signification |
+| --- | --- |
+| `0` | Succès sans avertissement. |
+| `2` | Exécution terminée avec avertissements : GPO non liée, DNS à vérifier, comparaison avec le serveur impossible, etc. Relire le journal. |
+| `1` | Erreur ; consulter le journal pour identifier les étapes déjà réalisées. |
+
+Après liaison et réplication de la GPO, vérifier sur un poste ciblé, dans PowerShell avec élévation :
+
+```powershell
+gpupdate /force
+Get-ScheduledTask -TaskName 'WAPT - waptdeploy' | Format-List TaskName, State, Principal
+```
+
+Adapter le nom de la tâche si `-TaskName` a été utilisé.
+
+Après chaque régénération de `waptagent.exe`, remplacer le fichier source et relancer la commande choisie pour actualiser les exécutables et le hash dans la GPO. Une GPO portant déjà le nom demandé est mise à jour.