#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