Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Secure Boot Certificates Power BI Dashboard : guide de déploiement avec Configuration Manager

Suite à l’expiration des certificats Secure Boot en juin, la visibilité sur l’état de conformité des PC est devenue un enjeu majeur.

Afin d’accompagner les équipes d’administration, nous vous partageons un tableau de bord Power BI dédié à Configuration Manager. Compatible avec les environnements Standalone et Co-Managed, il permet d’identifier rapidement les postes concernés, d’analyser les écarts de conformité et de faciliter le suivi des actions de remédiation.
Cet article présente les étapes de déploiement ainsi que les principales fonctionnalités de cette solution :

Prérequis – Tableau de bord Power BI Secure Boot Certificates

Téléchargement de la Configuration Baseline et du rapport Power BI.

Avant de commencer, téléchargez les éléments nécessaires :

Importation de la baseline de configuration Secure Boot

Par défaut, plusieurs informations nécessaires au suivi des certificats Secure Boot ne sont pas disponibles via Hardware Inventory de Configuration Manager.

L’utilisation d’une Configuration Baseline avec un état Compliant / Non-compliant constitue une méthode simple et efficace pour collecter ces informations et suivre la conformité des postes.
Étape 1 – Extraire les fichiers de la baseline Secure Boot Certificates

Étape 2 – Dans la console Configuration Manager, accédez à :
Actifs et conformité > Paramètres de conformité > Baselines de configuration, puis cliquez sur Importer des données de configuration.

Étape 3 – Cliquez sur Ajouter, puis sélectionnez tous les fichiers .cab extraits précédemment.
Étape 4 – Un avertissement relatif à un éditeur inconnu apparaît. Cliquez sur OK pour continuer.

Étape 5 – Cliquez sur Next afin de terminer l’assistant d’importation.

Étape 6 – Une fois créée, déployez la baseline de configuration Secure Boot Certificates.

Une fois créée, déployez la Secure Boot Certificates Configuration Baseline sur les collections de machines concernées.
Après un délai de 24 à 48 heures, vérifiez le nombre de machines Compliant / Non-compliant depuis la console Configuration Manager.

Secure Boot Certificates Power BI Dashboard

À présent, ouvrez le rapport Power BI téléchargé depuis Power BI Desktop afin de connecter les données et d’exploiter le tableau de bord Secure Boot Certificates.

Accept:

Indiquez le serveur SQL Configuration Manager et la base de données SQL utilisée:

Une demande d’autorisation pour la requête Native Database Query peut s’afficher. Cliquez sur OK pour continuer:

Une fois les requêtes terminées :

Script Powershell pour auditer les comptes desactivés, expirés ou proche de l’expiration

🔐 Mots de passe expirant prochainement : Anticipez les blocages des utilisateurs en identifiant qui doit changer son mot de passe sous $N$ jours.

⏱️ Comptes expirés : Repérez les comptes (souvent temporaires ou de prestataires) dont la date de fin de validité est dépassée.

🔒 Comptes désactivés : Listez les comptes inactifs afin de planifier leur suppression définitive.

Zéro bruit (Exclusions intelligentes) : Le script intègre un système de filtres automatiques pour ignorer les comptes de service (krbtgt, préfixes spécifiques SERV_, admapp_, etc.) et se concentrer uniquement sur les vrais comptes utilisateurs.

Rapport HTML : En plus de générer des fichiers CSV bruts pour chaque catégorie, le script compile les données dans un tableau HTML

Flexibilité : Vous pouvez ajuster les seuils de recherche directement via les paramètres (ex: chercher les mots de passe expirant dans 15 jours au lieu de 30).

#Requires -Modules ActiveDirectory
#Requires -Version 5.0

<#
.SYNOPSIS
    Génère un rapport consolidé des anomalies Active Directory
    
.DESCRIPTION
    Script qui regroupe tous les audits AD:
    - Utilisateurs avec mot de passe expirant prochainement
    - Comptes expirés depuis N jours
    - Comptes désactivés depuis N jours
    
    Génère des fichiers CSV et un rapport HTML consolid.

.PARAMETER DaysAheadPassword
    Nombre de jours à l'avance pour les mots de passe expirant.
    Défaut: 30

.PARAMETER DaysBackExpired
    Nombre de jours à remonter pour les comptes expirés.
    Défaut: 60

.PARAMETER DaysBackDisabled
    Nombre de jours à remonter pour les comptes désactivés.
    Défaut: 60

.PARAMETER OutputPath
    Répertoire de sortie pour les rapports.
    Défaut: C:\Temp\AD_Reports

.PARAMETER ExcludedObjectSid
    SID de l'objet à exclure (ex: BUILTIN\Invité).

.PARAMETER ExcludedPrefixes
    Tableau des préfixes de compte à exclure.

.EXAMPLE
    .\AD_Accounts_Disab_Expir_Audit.ps1
    
.EXAMPLE
    .\AD_Accounts_Disab_Expir_Audit.ps1 -DaysAheadPassword 15 -DaysBackExpired 90 -OutputPath "C:\Reports"
#>

param(
    [int]$DaysAheadPassword = 30,
    [int]$DaysBackExpired = 60,
    [int]$DaysBackDisabled = 60,
    [string]$OutputPath = "C:\Temp\AD_Reports",
    [string]$ExcludedObjectSid = "S-1-5-21-1801674531-261903793-725345543-501",
    [string[]]$ExcludedPrefixes = @("krbtgt", "TsInternetUser", "X_", "T_", "V_", "U_", "P_", "E_", "S_", "R_", "SERV_", "admapp_", "admpr_", "admex_")
)

# ============================================================================
# FUNCTIONS
# ============================================================================

function Write-Header {
    param([string]$Text)
    Write-Host ""
    Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "  $Text" -ForegroundColor Cyan
    Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
}

function Write-Section {
    param([string]$Text)
    Write-Host "➤ $Text" -ForegroundColor Yellow
}

function Write-Success {
    param([string]$Text)
    Write-Host "  ✓ $Text" -ForegroundColor Green
}

function Write-Error-Custom {
    param([string]$Text)
    Write-Host "  ✗ $Text" -ForegroundColor Red
}

function Build-ExclusionFilter {
    <#
    .SYNOPSIS
        Construit le filtre LDAP pour exclure les comptes de service
    #>
    $filterParts = @()
    foreach ($prefix in $ExcludedPrefixes) {
        $filterParts += "(!samaccountname=$prefix*)"
    }
    return ($filterParts -join "")
}

function Build-SelectedProperties {
    <#
    .SYNOPSIS
        Construit les propriétés à afficher dans le rapport
    #>
    return @(
        @{Name="Nom Affiche";Expression={$_."DisplayName"}},
        @{Name="Nom";Expression={$_."sn"}},
        @{Name="Prenom";Expression={$_."GivenName"}},
        @{Name="Login";Expression={$_."SamAccountName"}},
        @{Name="Description";Expression={$_."description"}},
        @{Name="Adresse email";Expression={$_."Mail"}},
        @{Name="Direction/Entite";Expression={$_."Division"}},
        @{Name="Service";Expression={$_."department"}},
        @{Name="Fonction";Expression={$_."title"}},
        @{Name="Bureau";Expression={$_."PhysicalDeliveryOfficeName"}},
        @{Name="Telephone fixe";Expression={$_."TelephoneNumber"}},
        @{Name="Numero IP";Expression={$_."IPPhone"}},
        @{Name="Telephone mobile";Expression={$_."Mobile"}},
        @{Name="Code taxation societe";Expression={$_."extensionAttribute9"}},
        @{Name="Code taxation service";Expression={$_."extensionAttribute10"}},
        @{Name="Date modification";Expression={$_."whenChanged"}}
    )
}

function Get-AllADProperties {
    <#
    .SYNOPSIS
        Retourne la liste complète des propriétés à récupérer
    #>
    return @(
        'DisplayName', 'sn', 'GivenName', 'SamAccountName', 'description', 
        'Mail', 'Division', 'department', 'title', 'PhysicalDeliveryOfficeName',
        'TelephoneNumber', 'IPPhone', 'Mobile', 'extensionAttribute9', 
        'extensionAttribute10', 'whenChanged', 'msDS-UserPasswordExpiryTimeComputed',
        'PasswordNeverExpires', 'Enabled', 'accountExpires'
    )
}

function Get-PasswordExpiringUsers {
    <#
    .SYNOPSIS
        Récupère les utilisateurs avec mots de passe expirant dans N jours
    #>
    Write-Section "Recherche des mots de passe expirant dans $DaysAheadPassword jours..."
    
    try {
        $DateStart = Get-Date
        $DateEnd = $DateStart.AddDays($DaysAheadPassword)
        
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        # Filtre pour les mots de passe expirant
        $filter = "(&(objectCategory=person)(objectClass=user)(enabled=TRUE)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(PasswordNeverExpires=TRUE))(!(objectSid=$ExcludedObjectSid))$exclusionFilter)"
        
        $allUsers = Get-ADUser -Filter {Enabled -eq $True -and PasswordNeverExpires -eq $False} -Properties $properties
        
        $results = @()
        foreach ($user in $allUsers) {
            $pwdExpiry = $user."msDS-UserPasswordExpiryTimeComputed"
            if ($pwdExpiry) {
                try {
                    $expiryDate = [DateTime]::FromFileTime($pwdExpiry)
                    if ($expiryDate -gt $DateStart -and $expiryDate -le $DateEnd) {
                        $userobj = $user | Select-Object (Build-SelectedProperties)
                        $userobj | Add-Member -MemberType NoteProperty -Name "Date expiration MdP" -Value ($expiryDate.ToString('dd/MM/yyyy')) -Force
                        $results += $userobj
                    }
                }
                catch { }
            }
        }
        
        Write-Success "$($results.Count) utilisateur(s) avec mot de passe expirant"
        return $results | Sort-Object "Date expiration MdP"
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Get-ExpiredAccounts {
    <#
    .SYNOPSIS
        Récupère les comptes expirés depuis N jours
    #>
    Write-Section "Recherche des comptes expirés depuis $DaysBackExpired jours..."
    
    try {
        $Date = (Get-Date).AddDays(-$DaysBackExpired)
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        $filter = "(&(!useraccountcontrol:1.2.840.113556.1.4.803:=2)(objectCategory=person)(objectClass=user)(accountExpires<=$($Date.ToFileTime()))(!(|(accountExpires=9223372036854775807)(accountExpires=0)))(!objectSid=$ExcludedObjectSid)$exclusionFilter)"
        
        $results = Get-ADUser -LDAPFilter $filter -Properties $properties |
            Where-Object {
                $ae = $_.accountExpires
                $ae -gt 0 -and $ae -ne 9223372036854775807
            } |
            ForEach-Object {
                $user = $_
                $expiryDate = [DateTime]::FromFiletime([Int64]($user.accountExpires))
                $userobj = $user | Select-Object (Build-SelectedProperties)
                $userobj | Add-Member -MemberType NoteProperty -Name "Date expiration" -Value ($expiryDate.ToString('dd/MM/yyyy')) -Force
                $userobj
            } |
            Sort-Object "Date expiration"
        
        Write-Success "$($results.Count) compte(s) expiré(s)"
        return @($results)
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Get-DisabledAccounts {
    <#
    .SYNOPSIS
        Récupère les comptes désactivés depuis N jours
    #>
    Write-Section "Recherche des comptes désactivés depuis $DaysBackDisabled jours..."
    
    try {
        $Date = (Get-Date).AddDays(-$DaysBackDisabled)
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        $filterParts = @(
            "(&(useraccountcontrol:1.2.840.113556.1.4.803:=2)(objectclass=user)"
            "(!objectSid=$ExcludedObjectSid)"
        )
        
        $filter = $filterParts -join "" + "$exclusionFilter)"
        
        $results = Get-ADUser -LDAPFilter $filter -Properties $properties |
            Select-Object (Build-SelectedProperties) |
            Sort-Object "Nom Affiche"
        
        Write-Success "$($results.Count) compte(s) désactivé(s)"
        return @($results)
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Export-ToCSV {
    <#
    .SYNOPSIS
        Exporte les données en fichier CSV
    #>
    param(
        [array]$Data,
        [string]$FileName
    )
    
    $filePath = Join-Path $OutputPath $FileName
    
    if ($Data -and $Data.Count -gt 0) {
        $Data | Export-Csv -Encoding UTF8 -NoTypeInformation -Path $filePath -Force
        Write-Success "Exporté: $FileName"
    }
    else {
        "Aucune donnée à exporter." | Out-File -Encoding UTF8 -FilePath $filePath -Force
        Write-Host "  ⚠ Exporté (vide): $FileName" -ForegroundColor Gray
    }
    
    return $filePath
}

function Generate-HTMLReport {
    <#
    .SYNOPSIS
        Génère un rapport HTML consolidé
    #>
    param(
        [array]$PasswordExpiringUsers,
        [array]$ExpiredAccounts,
        [array]$DisabledAccounts
    )
    
    Write-Section "Génération du rapport HTML..."
    
    $timestamp = Get-Date -Format "dd/MM/yyyy HH:mm:ss"
    $htmlPath = Join-Path $OutputPath "AD_Consolidated_Report.html"
    
    $totalIssues = ($PasswordExpiringUsers.Count) + ($ExpiredAccounts.Count) + ($DisabledAccounts.Count)
    
    $htmlContent = @"
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audit de l'expiration et desactivation des comptes Active Directory</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.3);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 40px 20px;
            text-align: center;
        }
        
        .header h1 {
            font-size: 32px;
            margin-bottom: 10px;
        }
        
        .header p {
            font-size: 14px;
            opacity: 0.9;
        }
        
        .content {
            padding: 30px 20px;
        }
        
        .summary-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            margin-bottom: 40px;
        }
        
        .summary-card {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            padding: 20px;
            border-radius: 8px;
            border-left: 4px solid #667eea;
            text-align: center;
        }
        
        .summary-card.warning {
            border-left-color: #ff6b6b;
            background: linear-gradient(135deg, #ffe5e5 0%, #ffcccc 100%);
        }
        
        .summary-card.danger {
            border-left-color: #ff4757;
            background: linear-gradient(135deg, #ffd6d6 0%, #ffb3b3 100%);
        }
        
        .summary-card.success {
            border-left-color: #2ed573;
            background: linear-gradient(135deg, #e5f9e5 0%, #ccf0cc 100%);
        }
        
        .summary-card h3 {
            font-size: 12px;
            text-transform: uppercase;
            color: #666;
            margin-bottom: 10px;
        }
        
        .summary-card .number {
            font-size: 28px;
            font-weight: bold;
            color: #333;
        }
        
        .section {
            margin-bottom: 40px;
        }
        
        .section h2 {
            border-bottom: 2px solid #667eea;
            padding-bottom: 10px;
            color: #333;
            margin-bottom: 20px;
            font-size: 20px;
        }
        
        table {
            width: 100%;
            border-collapse: collapse;
            background: white;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        
        th {
            background: #f8f9fa;
            color: #333;
            padding: 12px;
            text-align: left;
            font-weight: 600;
            border-bottom: 1px solid #ddd;
            font-size: 13px;
        }
        
        td {
            padding: 10px 12px;
            border-bottom: 1px solid #eee;
            font-size: 13px;
            color: #666;
        }
        
        tr:hover {
            background: #f8f9fa;
        }
        
        tr:last-child td {
            border-bottom: none;
        }
        
        .empty-message {
            text-align: center;
            padding: 30px;
            color: #999;
            background: #f8f9fa;
            border-radius: 8px;
        }
        
        .footer {
            background: #f8f9fa;
            padding: 20px;
            text-align: center;
            border-top: 1px solid #eee;
            color: #999;
            font-size: 12px;
        }
        
        .total-issues {
            font-size: 16px;
            font-weight: bold;
            color: #2c3e50;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📊 Audit de l'expiration et desactivation des comptes Active Directory</h1>
            <p>Généré le $timestamp</p>
        </div>
        
        <div class="content">
            <div class="total-issues">
                Nombre total de comptes détectés: <span style="color: #ff6b6b;">$totalIssues</span>
            </div>
            
            <div class="summary-grid">
                <div class="summary-card warning">
                    <h3>Mots de passe expirant</h3>
                    <div class="number">${($PasswordExpiringUsers.Count)}</div>
                </div>
                <div class="summary-card danger">
                    <h3>Comptes expirés</h3>
                    <div class="number">${($ExpiredAccounts.Count)}</div>
                </div>
                <div class="summary-card danger">
                    <h3>Comptes désactivés</h3>
                    <div class="number">${($DisabledAccounts.Count)}</div>
                </div>
            </div>
"@
    
    # Section Mots de passe expirant
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>🔐 Mots de passe expirant prochainement (dans $DaysAheadPassword jours)</h2>"
    
    if ($PasswordExpiringUsers -and $PasswordExpiringUsers.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date expiration MdP</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $PasswordExpiringUsers) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date expiration MdP')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun mot de passe n'expire dans les prochains $DaysAheadPassword jours</div>"
    }
    
    $htmlContent += "</div>"
    
    # Section Comptes expirés
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>⏱️ Comptes expirés (depuis $DaysBackExpired jours)</h2>"
    
    if ($ExpiredAccounts -and $ExpiredAccounts.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date expiration</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $ExpiredAccounts) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date expiration')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun compte expiré</div>"
    }
    
    $htmlContent += "</div>"
    
    # Section Comptes désactivés
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>🔒 Comptes désactivés (depuis $DaysBackDisabled jours)</h2>"
    
    if ($DisabledAccounts -and $DisabledAccounts.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date modification</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $DisabledAccounts) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date modification')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun compte désactivé</div>"
    }
    
    $htmlContent += "</div>"
    
    $htmlContent += @"
        </div>
        
        <div class="footer">
            <p>Rapport généré automatiquement par AD_Accounts_Disab_Expir_Audit.ps1</p>
            <p>$timestamp</p>
        </div>
    </div>
</body>
</html>
"@
    
    $htmlContent | Out-File -Encoding UTF8 -FilePath $htmlPath -Force
    Write-Success "Rapport HTML généré: AD_Consolidated_Report.html"
    
    return $htmlPath
}

# ============================================================================
# MAIN
# ============================================================================

try {
    # Vérification du module ActiveDirectory
    Write-Header "Audit de l'expiration et desactivation des comptes Active Directory"
    
    if (!(Get-Module ActiveDirectory)) {
        Write-Section "Import du module ActiveDirectory..."
        Import-Module ActiveDirectory -ErrorAction Stop
        Write-Success "Module ActiveDirectory importé"
    }
    
    # Créer le répertoire de sortie s'il n'existe pas
    if (!(Test-Path $OutputPath)) {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        Write-Success "Répertoire créé: $OutputPath"
    }
    else {
        Write-Success "Répertoire de sortie: $OutputPath"
    }
    
    Write-Header "Lancement des audits"
    
    # Récupérer les données
    $passwordExpiring = Get-PasswordExpiringUsers
    $expiredAccounts = Get-ExpiredAccounts
    $disabledAccounts = Get-DisabledAccounts
    
    Write-Header "Exportation des données"
    
    # Exporter en CSV
    Export-ToCSV -Data $passwordExpiring -FileName "PasswordExpiring-$($DaysAheadPassword)Days.csv"
    Export-ToCSV -Data $expiredAccounts -FileName "AccountExpired-$($DaysBackExpired)Days.csv"
    Export-ToCSV -Data $disabledAccounts -FileName "AccountDisabled-$($DaysBackDisabled)Days.csv"
    
    # Générer le rapport HTML
    Write-Header "Génération du rapport"
    $reportPath = Generate-HTMLReport -PasswordExpiringUsers $passwordExpiring -ExpiredAccounts $expiredAccounts -DisabledAccounts $disabledAccounts
    
    # Résumé final
    Write-Host ""
    Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Green
    Write-Host "║                   ✓ AUDIT TERMINÉ AVEC SUCCÈS              ║" -ForegroundColor Green
    Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Green
    Write-Host ""
    Write-Host "Résumé:" -ForegroundColor Cyan
    Write-Host "  • Mots de passe expirant: $($passwordExpiring.Count)" -ForegroundColor Yellow
    Write-Host "  • Comptes expirés: $($expiredAccounts.Count)" -ForegroundColor Red
    Write-Host "  • Comptes désactivés: $($disabledAccounts.Count)" -ForegroundColor Red
    Write-Host "  • Total comptes: $(($passwordExpiring.Count) + ($expiredAccounts.Count) + ($disabledAccounts.Count))" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Fichiers générés dans: $OutputPath" -ForegroundColor Green
    Write-Host ""
    
    # Proposer d'ouvrir le rapport
    $response = Read-Host "Voulez-vous ouvrir le rapport HTML? (O/N)"
    if ($response -eq "O" -or $response -eq "o") {
        Start-Process $reportPath
    }
}
catch {
    Write-Host ""
    Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Red
    Write-Host "║                    ✗ ERREUR FATALE                        ║" -ForegroundColor Red
    Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Red
    Write-Host ""
    Write-Error $_
    exit 1
}

PoshTorch : module Powershell pour exporter des métriques Prometheus

Plusieurs modules Powershell développés par la communauté autour du besoin d’exporter des métriques au format Prometheus depuis des scripts Powershell existent déjà mais aucun ne répondait précisément à mon besoin, faire le strict minimum à l’aide de la syntaxe la plus simple et claire possible:

  • Créer des métriques
  • Leur ajouter une ou plusieurs valeurs avec ou sans labels
  • Formater le résultat au format Prometheus
  • Exporter le résultat dans le terminal (StdOut), vers un fichier texte (avec écriture atomique) qui puisse être lu par un Node Exporter Prometheus local ou enfin vers une PushGateway.

Nul besoin d’ajouter la gestion de l’affichage des métriques directement en http local dans ce module : cela rajoute une forte couche de complexité non liée au besoin initial et de vrais risques de sécurité, alors ce rôle peut très bien être assumé par un module dédié comme le très complet Pode combiné à la sortie en StdOut comme nous le verrons plus loin.

J’ajouterais qu’il s’agissait aussi d’un bon prétexte pour tester plus sérieusement les classes de Powershell.

Trève de bavardages, commençons par le lien vers le repo GitHub du module (pas encore disponible sur la PSGallery) : https://github.com/Cyr-Az/poshtorch

Voyons maintenant comment fonctionne le module :

Avant d’exporter les données, il est nécessaire de créer les objets correspondants en et de leur assigner des valeurs. Le module s’appuie sur deux commandes principales pour ces opérations :

  • New-PrometheusMetric : Initialise l’objet métrique avec ses métadonnées de base (nom, description et type). Le nom de la métrique est automatiquement validé pour respecter les standards de Prometheus (caractères alphanumériques et underscores uniquement). Cette commande supporte les paramètres suivants :
    • Name : le nom technique de la métrique
    • Help : une description textuelle lisible par un humain
    • Type : le type de métrique, limité aux valeurs conformes à Prometheus (counter, gauge, histogram ou summary).
  • Add-PrometheusMetricValue : Attache une valeur numérique à une métrique préalablement créée. Cette fonction peut être appelée plusieurs fois sur la même métrique pour générer des séries temporelles multiples (Time Series). Elle supporte les paramètres suivants :
    • Metric : l’objet métrique à modifier, généralement passé via le pipeline
    • Value : la valeur numérique
    • Labels : paramètre optionnel acceptant une hashtable @{ clé = « valeur » } pour définir les étiquettes associées à cette valeur.

Une fois les objets métriques créés via New-PrometheusMetric et Add-PrometheusMetricValue, le module propose trois fonctions distinctes pour restituer  (exporter) la donnée selon vos besoins :

  • Out-PrometheusMetric : Convertit l’objet PowerShell en une chaîne de caractères (texte brut) respectant le format d’exposition Prometheus (version 0.0.4). Elle est utilisée pour le debug dans la console ou pour transmettre le texte à un serveur HTTP. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
  • Export-PrometheusFile : Sauvegarde les métriques dans un fichier texte (souvent avec l’extension .prom). Cette méthode est conçue pour être couplée au Textfile Collector de l’agent Node Exporter. La fonction utilise un fichier temporaire qu’elle remplace de manière atomique (uniquement en Powershell 7+) afin d’éviter que Prometheus ne scrape un fichier en cours d’écriture. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
    • Path : le chemin complet du fichier de destination .
  • Export-PrometheusPushgateway : Envoie directement les métriques à une instance Prometheus Pushgateway via une requête http(s). C’est l’approche recommandée pour les scripts éphémères (ex: tâches planifiées ou scripts de sauvegarde) qui ne peuvent pas être interrogés activement par Prometheus. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
    • GatewayUrl : l’adresse de la Pushgateway, ex: http://localhost:9091
    • Job : le nom du job ciblé 
    • Instance : optionnel, pour spécifier le nom de l’instance
    • Method : PUT pour remplacer le précédent batch de métriques recu par la PushGateway (valeur par défaut) ou POST pour ajouter les données au précédent batch de métriques.

Terminons enfin cet article par quelques exemples d’utilisation illustrant le fonctionnement du module dans des scénarios d’administration courants :

Utilisation avec le pipeline PowerShell

Les fonctions du module sont conçues pour supporter le pipeline. Cela permet d’enchaîner la création de la métrique, l’ajout de la valeur et l’export sans nécessiter de variables intermédiaires, ce qui est particulièrement adapté pour l’export de métriques simples, pour une seule instance/un seul objet ou sans label particulier.

Dans l’exemple ci-dessous, le script récupère le nombre de comptes Active Directory verrouillés et l’écrit directement dans un fichier .prom destiné à être lu par le Textfile Collector d’un Node Exporter :

Import-Module -Name ActiveDirectory

Import-Module -Name PoshTorch

# Récupération de la donnée

$lockedAccounts = (Get-ADUser -Filter {LockedOut -eq $true}).Count

# Création, ajout de la valeur et export vers le fichier

New-PrometheusMetric -Name 'ad_locked_accounts_total' -Help 'Comptes AD verrouillés' -Type gauge |

    Add-PrometheusMetricValue -Value $lockedAccounts |

    Export-PrometheusFile -Path 'C:\Metrics\ad_health.prom'

Gestion des labels et des séries multiples

Prometheus utilise des labels pour ajouter de la dimension aux métriques, ce qui permet de stocker plusieurs séries temporelles (Time Series) sous un même nom. PoshTorch gère ces labels via les hashtables standards de PowerShell.

L’exemple suivant vérifie l’état de plusieurs services Windows. La métrique est créée une seule fois, puis alimentée itérativement avant d’être envoyée à une PushGateway :

Import-Module -Name PoshTorch

$criticalServices = @("wuauserv", "Spooler", "WinRM")

# Initialisation de l'objet métrique

$serviceMetric = New-PrometheusMetric -Name 'windows_service_status' `

                                      -Help '1 si en cours, 0 si arrêté' `

                                      -Type gauge

foreach ($serviceName in $criticalServices)

{   

    $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue

    $isUp = if ($service.Status -eq 'Running') { 1 } else { 0 }

    # Définition des labels pour l'itération courante

    $serviceLabels = @{

        service_name = $serviceName

        start_type   = $service.StartType.ToString()

        host         = $env:COMPUTERNAME

    }

    # Ajout de la valeur à la métrique

    $serviceMetric = $serviceMetric | Add-PrometheusMetricValue -Value $isUp -Labels $serviceLabels

}

# Envoi de la métrique contenant les données des trois services

$serviceMetric | Export-PrometheusPushgateway -GatewayUrl 'http://pushgateway.corp.local:9091' `

                              -Job 'service_monitoring'

Exposer les métriques en HTTP avec Pode

Si l’architecture de supervision nécessite que le script s’exécute comme un exporter classique interrogeable par Prometheus (modèle pull), la fonction Out-PrometheusMetric permet de récupérer les données formatées en texte brut.

Il suffit ensuite de lier ce flux à un module de serveur web dédié comme Pode.

Déléguer l’écoute HTTP à Pode permet de conserver un module de métriques strictement limité à la mise en forme des données, tout en laissant l’opportunité de s’appuyer sur les fonctionnalités d’un vrai serveur web (HTTPS, gestion des certificats, authentification, logs de requêtes) si le besoin s’en fait sentir.

L’exemple suivant expose la consommation de mémoire vive sur un endpoint /metrics :

Import-Module Pode

Import-Module PoshTorch

Start-PodeServer {

    # Listen on port 8080

    Add-PodeEndpoint -Address * -Port 8080 -Protocol Http

    # Define the /metrics route that Prometheus will scrape

    Add-PodeRoute -Method Get -Path '/metrics' -ScriptBlock {

        # Generate your metrics

        $metric = New-PrometheusMetric -Name 'ps_script_duration_seconds' `

                                    -Help 'Execution time of the backup script' `

                                    -Type gauge

        $metric | Add-PrometheusMetricValue -Value 45.2 -Labels @{ status = "success"; task = "db_backup" }

        $metric | Add-PrometheusMetricValue -Value 12.0 -Labels @{ status = "failed";  task = "log_cleanup" }

        # Génération du format texte Prometheus via StdOut

        $payload = $metric | Out-PrometheusMetric

# Renvoi de la réponse HTTP avec l'en-tête attendu par Prometheus

        Write-PodeTextResponse -Value $payload -ContentType 'text/plain; version=0.0.4'

    }

}

Au fait, vous vous demandez peut-être pourquoi le nom PoshTorch ? Direction le readme du repo Github pour la réponse !