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 !