Ajoute le déploiement WAPT par GPO et son guide d'utilisation
This commit is contained in:
1 parent
7b10be05d1
commit
69e00defcc
2 files changed
+786
-1
No files matched your search
@@ -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)
|
||||
@"
|
||||
<?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
|
||||
Reference in new issue
Block a user