Files
comi-deploy-waptagentgpo/Deploy-WaptAgentGpo.ps1
T

697 lines
31 KiB
PowerShell

#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)
@"
<?xml version="1.0" encoding="utf-8"?>
<Files clsid="{215B2E53-57CE-475c-80FE-9EEC14635851}">
<File clsid="{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}" name="waptdeploy.exe" status="waptdeploy.exe" image="1" changed="$now" uid="$(New-GppUid)">
<Properties action="R" fromPath="$deploySrc" targetPath="$deployDst" readOnly="1" archive="1" hidden="0" suppress="0"/>
</File>
<File clsid="{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}" name="waptagent.exe" status="waptagent.exe" image="2" changed="$now" uid="$(New-GppUid)">
<Properties action="U" fromPath="$agentSrc" targetPath="$agentDst" readOnly="1" archive="1" hidden="0" suppress="0"/>
</File>
</Files>
"@
}
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.
@"
<?xml version="1.0" encoding="utf-8"?>
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">
<TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="$name" image="1" changed="$now" uid="$(New-GppUid)" userContext="0" removePolicy="0">
<Properties action="R" name="$name" runAs="S-1-5-18" logonType="S4U">
<Task version="1.3">
<RegistrationInfo>
<Author>$auth</Author>
<Description>Deploiement de l'agent WAPT via waptdeploy.exe (GPO tache planifiee)</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<LogonType>S4U</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<IdleSettings>
<Duration>PT10M</Duration>
<WaitTimeout>PT1H</WaitTimeout>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<AllowStartOnDemand>false</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Triggers>
<CalendarTrigger>
<Repetition>
<Interval>PT1H</Interval>
<Duration>P1D</Duration>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>$start</StartBoundary>
<ExecutionTimeLimit>PT2H</ExecutionTimeLimit>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Actions Context="Author">
<Exec>
<Command>$cmd</Command>
<Arguments>$args_</Arguments>
</Exec>
</Actions>
</Task>
</Properties>
</TaskV2>
</ScheduledTasks>
"@
}
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