Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Azure – Utiliser des packages DSC génériques dans Machine Configuration

Le service Azure Machine Configuration (anciennement connu sous le nom de Guest Configuration) est une fonctionnalité qui parait encore méconnue ou boudée au profit d’autres mécanismes comme Ansible. Elle est pourtant la digne descendante du vénérable  PowerShell DSC (Desired State Configuration). et de ses implémentations Azure (extension DSC et Azure Automation DSC, dont la dépréciation a été annoncée pour 2027) : elle permet d’auditer et de configurer l’intérieur de vos machines virtuelles (paramètres de l’OS, registres, applications) à l’aide de packages constitués de modules et de configurations DSC classiques accessibles en HTTPS (souvent via un storage account).

A la différence des méthodes précédentes, Machine Configuration n’utilise pas le Local Configuration Manager (LCM) natif de Windows mais plutôt une extension Azure dédiée qui se charge de la récupération des packages, de l’application des configurations qu’ils contiennent et de la remontée des informations d’audit. Machine Configuration permet également d’appliquer plusieurs configurations (assignments) à une machine, alors que les méthodes traditionnelles ne pouvaient prendre en compte qu’une seule.

Un défaut historique de DSC réside dans ses configurations qui sont des fichiers .mof statiques, obtenus après « compilation » d’un script Powershell DSC. Il était donc nécessaire de créer un fichier .mof pour chaque serveur, au lieu de pouvoir simplement utiliser une configuration générique et lui passer les paramètres propres à chaque serveur.

Mais réjouissez-vous : c’est de l’histoire ancienne! Même si Machine Configuration repose toujours sur des packages contenant des .mof compilés et donc des valeurs statiques, il introduit la possibilité de les modifier directement via la propriété configurationParameter de la ressource guestConfigurationAssignments. Autrement dit, il est possible de créer des .mof contenant les ressources nécessaires à la configuration d’un role sur vos serveurs (par exemple IIS) avec une série de paramètres standardisés, mais avec des valeurs fictives; puis de modifier ces valeurs à la volée dans vos templates ARM en fonction du serveur auxquels vous appliquez les guestConfigurationAssignments!

Concrètement, comment cela fonctionne-t-il ? Lors de l’assignation de la configuration via l’API Azure (par exemple lors du déploiement de votre template ARM), le service transmet vos paramètres à l’agent local (l’extension Azure présente sur la VM). Ce dernier va alors lire le fichier MOF téléchargé et écraser littéralement les valeurs textuelles statiques avec celles que vous avez fournies, juste avant l’application de l’état désiré. Vous n’avez plus besoin d’exécuter la moindre commande à l’intérieur de la machine ou de regénérer un package par environnement. Un seul package IIS.zip compilé une bonne fois pour toutes suffit désormais pour tous vos serveurs !

Pour bien visualiser, voici à quoi ressemblerait le code source de cette configuration DSC générique (avant sa compilation en fichier .mof). On y installe le rôle IIS natif, et on y configure un site web basique à l’aide du module WebAdministrationDsc. Remarquez comment les valeurs utilisées sont fictives (placeholders) et n’ont pas vocation à rester telles quelles en production :

Configuration IIS {
    # Import des ressources natives et du module IIS
    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    Import-DscResource -ModuleName 'WebAdministrationDsc'

    Node localhost {
        # 1. Installation du rôle IIS
        WindowsFeature InstallIIS {
            Ensure = "Present"
            Name   = "Web-Server"
        }

        # 2. Création et paramétrage du site web (avec des valeurs fictives)
        WebSite MyCompanySite {
            Ensure       = "Present"
            Name         = "MySite"
            State        = "Started"
            PhysicalPath = "C:\inetpub\wwwroot\dummy" # Valeur statique destinée à être écrasée
            DependsOn    = "[WindowsFeature]InstallIIS"
        }
    }
}

Pour modifier une de ces valeurs (par exemple PhysicalPath) ou même le statut du site (State) à la volée dans vos templates ARM, il faut utiliser le paramètre configurationParameter de votre ressource  Microsoft.Compute/virtualMachines/providers/guestConfigurationAssignments. Attention, la syntaxe n’est pas très intuitive : la clé name du paramètre doit cibler précisément la ressource DSC et sa propriété selon le modèle suivant : [ClasseDeLaRessource]NomDeLaRessource;NomDeLaPropriete.

En reprenant le code DSC ci-dessus (la ressource nommée MyCompanySite de classe WebSite), pour définir le véritable chemin d’accès aux fichiers du site pour un serveur spécifique de production, la définition dans le template ARM inclura ce bloc JSON :

"properties": {
  "guestConfiguration": {
    "name": "IIS",
    "assignmentType": "ApplyAndMonitor",
    "configurationParameter":[
      {
        "name": "[WebSite]MyCompanySite;PhysicalPath",
        "value": "D:\ProductionWeb\MySite"
      }
    ]
  }
}

Attention cependant : tous les paramètres passés à Machine Configuration doivent être de type string. L’API Azure ne supporte pas le passage de tableaux (arrays) via la propriété configurationParameter et ce, même si la ressource DSC sous-jacente est techniquement capable de les interpréter (comme c’est le cas avec la propriété BindingInfo du module WebAdministrationDsc par exemple).

Si ce mécanisme vous semble familier, c’est normal : c’est en effet exactement le même qui se cache derrière les « Azure Security Baselines » déployées via Azure Policy. Lorsque vous configurez une policy Azure pour imposer des paramètres d’OS à votre parc, le service se contente d’orchestrer la création de ces assignations en injectant dynamiquement vos valeurs dans les fichiers MOF pré-packagés par Microsoft.

Le même résultat est bien sûr atteignable à l’aide de Terraform, mais cela sera l’objet du prochain article!

Powershell – Script custom d’application des MAJ Windows avec gestion du reboot

J’ai eu a creer un script un peu customisé pour gerer l’application de patch windows, pour des machines qui ne peuvent pas etre géré automatiquement. Dans mon contexte cette action devait en plus etre associé a la creation d’un snapshot de machine virtuelle, apres la mise a jour de cette machine.

Le script:

  • vérifie si des mises à jour sont réellement en attente ou si un reboot est déjà nécessaire.
  • lance le processus d’installation en arrière-plan (via un Job) pour ne pas bloquer votre session.
  • Si l’option -reboot est donnée, il redémarre la machine et attend qu’elle soit vraiment prête. Apres le reboot Il attends a la fois la disponibilité de la connexion winrm et l’occurence de l’event ID 6009 (validant que la machine viens de redemarrer) avant de valider le fait que la machine a bien redémarré.

#####################################################################################################################
#
# check_and_apply_windows_updates.ps1
# 
# Ce script:
# - Verifie sur un serveur cible si il y a des mise a jour Windows Update en attente ou un reboot en attente
# - Execute ces mise a jour 
# - Gere ou pas le reboot s'il doit avoir lieu (option -reboot) (attente que la machine cible soit joignable)
########################################################################################################################

<# Exemples

Sans reboot :
.\check_and_apply_windows_updates.ps1 -Target MyServer

Avec reboot si requis suite au patching :
.\check_and_apply_windows_updates.ps1 -Target MyServer -reboot

#>

################# Declaration des variables et fonctions ###############################################

Param(
[ValidateNotNullOrEmpty()]$Target,
[switch]$reboot,
$SecTimeOutForReboot=300,
$SecRetryIntervalReboot=10
)


$Log = "$PSScriptRoot\Launch_Maj.log"
$ScriptMaj = "$env:LOCALAPPDATA\check_and_apply_windows_updates.ps1"


$getUpdateParam = @{            
            NameSpace = 'root/ccm/ClientSDK'
            ClassName = 'CCM_SoftwareUpdate'
            Filter = 'EvaluationState < 8'
            }   

$ContScriptMaj = @'
$installUpdateParam = @{
        NameSpace = 'root/ccm/ClientSDK'
        ClassName = 'CCM_SoftwareUpdatesManager'
        MethodName = 'InstallUpdates'
    }

    $getUpdateParam = @{            
        NameSpace = 'root/ccm/ClientSDK'
        ClassName = 'CCM_SoftwareUpdate'
        Filter = 'EvaluationState < 8'
    }       

    [ciminstance[]]$updates = Get-CimInstance @getUpdateParam
    
    if ($updates) {
        Invoke-CimMethod @installUpdateParam  -Arguments @{ CCMUpdates = $updates } 
        
        while(Get-CimInstance @getUpdateParam){
            Start-Sleep -Seconds 30
        }
    }

'@

Function Add-ToLog {
Param ([string]$Text,
[string]$Couleur="Green"
 )
 #Write-Output "[LOG] $Text"
 $Text = "[$([DateTime]::Now)] - $Text"
 
 # Retry logic for file contention when multiple scripts write simultaneously
 $maxRetries = 10
 $retryCount = 0
 $retryDelayMs = 100
 
 while ($retryCount -lt $maxRetries) {
     try {
         Add-Content -Path $Log -Value $Text -ErrorAction Stop
         break
     } catch {
         $retryCount++
         if ($retryCount -lt $maxRetries) {
             Start-Sleep -Milliseconds $retryDelayMs
         } else {
             Write-Host "[LOG WARNING] Could not write to log file after $maxRetries attempts (file may be locked)" -ForegroundColor Yellow
         }
     }
 }
 
 Write-Host $Text -ForegroundColor $Couleur
}


Function Check-IfPendingUpdates($Target)
{
[ciminstance[]]$updates = Invoke-Command -ComputerName $Target -ScriptBlock {$getUpdateParam = $using:getUpdateParam;Get-CimInstance @getUpdateParam}
if ($updates)
    {
    return $updates
    }
else
    {
    return $null
    }
}


Function Check-IfRebootPending($Target)
{
$RebootPending = (Invoke-Command -ComputerName $Target -ScriptBlock {Invoke-CimMethod -Namespace root/ccm/ClientSDK -ClassName CCM_ClientUtilities -MethodName DetermineIfRebootPending}).Rebootpending
if ($RebootPending)
    {
    return $True
    }
else
    {
    return $false
    }
}



function Wait-ForWinRMAndEvent {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)] [string] $ComputerName,
        [Parameter()] [int] $TimeoutMinutes = 15,
        [Parameter()] [int] $PollSeconds = 5,
        [Parameter()] [datetime] $RebootStartTime = ((Get-Date).ToUniversalTime()),
        [Parameter()] [int] $ToleranceSeconds = 60,
        [Parameter()] [int] $global:EventToFind = 6009
    )

    $deadline = (Get-Date).AddMinutes($TimeoutMinutes)
    Write-Verbose "Waiting for $ComputerName WinRM and EventID $EventToFind (timeout $TimeoutMinutes min, poll every $PollSeconds s)"

    while ((Get-Date) -lt $deadline) {
        # 1) Test WinRM reachability
        $winrmOk = $false
        try {
            # Test-WSMan uses WinRM; returns successfully when WSMan endpoint responsive
            Test-WSMan -ComputerName $ComputerName -ErrorAction Stop | Out-Null
            $winrmOk = $true
        } catch {
            $winrmOk = $false
        }

        if ($winrmOk) {
            Write-Verbose "WinRM reachable on $ComputerName, checking EventID $EventToFind..."
            try {
                # Le ScriptBlock utilise $using:EventToFind pour transmettre la variable dans la session distante
                $script = {
                    # Récupère la dernière occurrence de l'EventID $using:EventToFind dans System
                    $evt = Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = $using:EventToFind } -MaxEvents 1 -ErrorAction SilentlyContinue
                    if ($null -eq $evt) { return $null }
                    # Retourner le TimeCreated en UTC (format ISO)
                    return $evt.TimeCreated.ToUniversalTime().ToString("o")
                }

                $lastIso = Invoke-Command -ComputerName $ComputerName -ScriptBlock $script -ErrorAction Stop

                # Invoke-Command peut renvoyer un tableau ; prendre la première valeur utile
                if ($lastIso -is [array] -and $lastIso.Length -gt 0) { $lastIso = $lastIso[0] }

                if ($lastIso) {
                    # Parse en datetime UTC
                    try {
                        $evtDt = [datetime]::Parse($lastIso).ToUniversalTime()
                    } catch {
                        Write-Verbose "Impossible de parser l'ISO renvoye: $lastIso"
                        $evtDt = $null
                    }

                    if ($evtDt) {
                        $threshold = $RebootStartTime.AddSeconds(-1 * [int]$ToleranceSeconds)
                        if ($evtDt -ge $threshold) {
                            Write-Verbose "EventID $EventToFind trouve a $evtDt (>= $threshold). Condition satisfaite."
                            return $true
                        } else {
                            Write-Verbose "EventID $EventToFind trouve mais antérieur au démarrage attendu: $evtDt < $threshold"
                        }
                    } else {
                        Write-Verbose "Aucune date d'evenement valide retournee."
                    }
                } else {
                    Write-Verbose "Aucun EventID $EventToFind trouve encore."
                }

            } catch {
                Write-Verbose "Erreur lors de la requete d'evenements sur $ComputerName : $_"
                # On continue (peut-être WinRM venait juste de s'ouvrir et l'auth a échoué)
            }
        } else {
            Write-Verbose "WinRM non joignable sur $ComputerName (probablement toujours en reboot)."
        }

        Start-Sleep -Seconds $PollSeconds
    }

    throw "Timeout: $ComputerName n'a pas repondu à WinRM ET/OU n'a pas loggue l'EventID $EventToFind dans les $TimeoutMinutes minutes."
}

################# Debut du script ###############################################

#Test d'acces au serveur cible
 
    Try
    {
    Test-WSMan -ComputerName $Target -ErrorAction Stop | Out-Null
    }
    catch
    {
    Add-Tolog "$Target est injoignable" -Couleur Red
    exit 1
    }


 
    # Message de connexion
    $message = "Connexion au serveur $Target..."
    Add-ToLog -text $message -Couleur White

    # Si il y a des updates en attente on les applique 
    $updates = Check-IfPendingUpdates -Target $Target
    If ($updates)
            {
            $message = "Il y a $(@($updates).count) mises a jour a faire sur $Target"
            Add-ToLog -Text $message -Couleur White
            $message = "Installation des mises a jour sur le serveur $Target..." 
            Add-ToLog -text $message -Couleur White
            Set-Content -Value $ContScriptMaj -Path $ScriptMaj -Force
            Invoke-Command -ComputerName $Target -FilePath "$ScriptMaj" -AsJob -JobName "$($Target)_job"

            # On attend la fin de l'execution des mise a jours
            Add-Tolog "En attente de la fin de la mise a jour de $Target, cette etape peut prendre plus d'une heure..." -couleur Yellow
            $job = Get-Job -Name "$($Target)_job"
            Wait-Job $job | Out-Null
            Remove-Job $job | Out-Null
            # Si le job est est OK - Fin de l'installation des updates
            If ($job[-1].state -like "Completed")
                {
                $message = "$Target - Fin de l'installation des mises a jour"
                Add-ToLog -Text $message -Couleur White
                }
            # Si le job est est KO - On affiche le probleme et Fin du script
            Else
                {
                $message = "$Target - Erreur pendant l'installation de une ou plusieurs mise a jour - Etat du job: $($job[-1].state) - Verifier l'Etat de la machine"
                Add-ToLog $message -Couleur Red
                Exit 1
                #$MajJobState = "Error"
                }
        
            # On verifie si les updates appliques requiert une redemarrage
            If (Check-IfRebootPending -Target $Target)
                {
                $message = "$Target : Il y a des mises a jour installees qui necessitent un redemarrage du serveur !"
                #write-host $message -F Yellow
                Add-ToLog $message -Couleur Yellow
                If ($reboot.IsPresent -eq $true)
                    {
                    $message = "Redemarrage du serveur $Target pour integrer les mises a jours...Le script attendra que le serveur $Target soit a nouveau joignable"
                    Add-ToLog $message -Couleur Yellow
                    #write-host $message -F Yellow
                    # LE RESTART-COMPUTER NE FONCTIONNE PAS!!!
                    # Restart-Computer -ComputerName $Target -Force -Wait -For PowerShell -Delay 5 -Timeout 300 -WhatIf
                    # Execution du reboot
                    Invoke-Command -ComputerName $target -ScriptBlock {shutdown.exe /f /r /c \"Reboot suite a installation de Mise a jour"}
                    # On attend que la machine ne soit plus joignable
                    Start-Sleep -Seconds 60

                # On attend que ça reboot
                $endTime = (Get-Date).AddSeconds($SecTimeOutForReboot)
                # tant que la date actuelle est inferieure a la date de fin
                while ((Get-Date) -lt $endTime)
                {
                
                try {
                    Wait-ForWinRMAndEvent -ComputerName $target -RebootStartTime $((Get-Date).ToUniversalTime()) -TimeoutMinutes 10 -PollSeconds 10 -Verbose
                    Write-Host "Machine $target de retour et EventID $EventToFind detecte."
                    # sortie immédiate de la boucle dès que la condition est satisfaite
                    break
                    } 
                catch 
                    {
                    Write-Warning $_
                    exit 1
                            }
                        }
                    }
                Else
                    {
                    $message = "Le redemarrage du serveur $Target n'a pas ete demande, reexecuter le script avec l'option -reboot pour que le serveur ne reste pas dans l'etat REBOOTPENDING"
                Add-ToLog $message -Couleur Yellow
                    #write-host $message -F Yellow
                    }

                }


       }
 



    # Si il y a un reboot en attente Meme sans MAJ ET si l'option reboot est precisee on redemarre
    ElseIf (Check-IfRebootPending -Target $Target)
        {
        $message = "$Target : Il y a des mises a jour installees qui necessitent un redemarrage du serveur !"
        #write-host $message -F Yellow
        Add-ToLog $message -Couleur Yellow
            If ($reboot.IsPresent -eq $true)
                {
                $message = "Redemarrage du serveur $Target...Le script attendra que le serveur $Target soit a nouveau joignable"
                Add-ToLog $message -Couleur Yellow
                #write-host $message -F Yellow
                # LE RESTART-COMPUTER NE FONCTIONNE PAS!!!
                # Restart-Computer -ComputerName $Target -Force -Wait -For PowerShell -Delay 5 -Timeout 300 -WhatIf
                # Execution du reboot
                Invoke-Command -ComputerName $target -ScriptBlock {shutdown.exe /f /r /c \"Reboot suite a installation de Mise a jour"}
                # On attend que la machine ne soit plus joignable
                Start-Sleep -Seconds 60

                # On attend le reboot
                $endTime = (Get-Date).AddSeconds($SecTimeOutForReboot)
                # tant que la date actuelle est inferieure a la date de fin
                while ((Get-Date) -lt $endTime)
                {
                
                try {
                    Wait-ForWinRMAndEvent -ComputerName $target -RebootStartTime $((Get-Date).ToUniversalTime()) -TimeoutMinutes 10 -PollSeconds 10 -Verbose
                    Write-Host "Machine $target de retour et EventID $EventToFind detecte."
                    # sortie immédiate de la boucle dès que la condition est satisfaite
                    break
                    } 
                catch 
                    {
                    Write-Warning $_
                    exit 1
                            }
                        }
            
            Else
                {
                $message = "Le redemarrage du serveur $Target n'a pas ete demande, reexecuter le script avec l'option -reboot pour que le serveur ne reste pas dans l'etat REBOOTPENDING"
                Add-ToLog $message -Couleur Yellow
                #write-host $message -F Yellow
                }

        }
        }

    # Si il n'y a pas d'update en attente et pas de reboot en attente
    Else {
        $message = "OK - Pas de mise a jour en attente et pas de reboot en attente"
        Add-ToLog -text $message -Couleur White
        start-sleep -Seconds 60
         Exit 0
         }







[Entra ID] Récupération des différents types de sign-in pour un audit de sécurité

Lors d’un audit de sécurité Entra ID (Azure AD), il est essentiel de distinguer les différents types de connexions afin d’avoir une vision claire des usages et d’identifier les connexions suspectes ou inutiles. Microsoft Graph centralise ces informations, mais elles ne sont pas toujours triviales à exploiter directement.

En pratique, quatre types principaux de sign-ins sont à connaître :

  • Interactive user
    Une connexion initiée par un utilisateur réel, typiquement via le portail Microsoft 365, Outlook, Teams ou un autre service nécessitant l’authentification interactive.
    Ces sign-ins sont soumis aux politiques MFA et aux conditions d’accès.

  • Non-interactive user
    Une connexion réalisée par un utilisateur, mais sans interaction directe, souvent pour des scripts, applications ou services automatisés utilisant un mot de passe ou un token stocké.
    Exemples : tâches planifiées, scripts PowerShell ou outils tiers accédant à Exchange ou SharePoint via un compte utilisateur.

  • Service Principal (SPN)
    Une authentification effectuée par une application enregistrée dans Entra ID. Les SPN sont utilisés par des services et scripts qui se connectent au nom de l’application, indépendamment d’un utilisateur.
    Exemples : intégrations SaaS, scripts automatisés via une App Registration, pipelines CI/CD.

  • Managed Identity
    Utilisée par les ressources Azure (VM, Functions, App Service, etc.) pour s’authentifier automatiquement auprès d’Azure AD sans gérer de credentials.
    Ces identités sont sécurisées, éphémères et ne nécessitent pas de mot de passe stocké.

Microsoft Graph ne propose pas un filtre unique pour tous les distinguer facilement. Pour obtenir une vue précise, il faut filtrer les sign-ins selon l’attribut signInEventTypes.

Prérequis

Commandes PowerShell par type de sign-in

Pour récupérer les sign-ins d’une application ou d’un utilisateur, on utilise la cmdlet Get-MgBetaAuditLogSignIn avec un filtre sur signInEventTypes :

$StartDate = (Get-Date).AddDays(-30)
$Application = Get-MgApplication -Filter "DisplayName eq 'MonApp'"

# Interactive
Get-MgBetaAuditLogSignIn -Filter "AppId eq '$($Application.AppId)' and createdDateTime ge $StartDate and signInEventTypes/any(t: t eq 'interactiveUser')"

# Non-interactive
Get-MgBetaAuditLogSignIn -Filter "AppId eq '$($Application.AppId)' and createdDateTime ge $StartDate and signInEventTypes/any(t: t eq 'nonInteractiveUser')"

# Service Principal
Get-MgBetaAuditLogSignIn -Filter "AppId eq '$($Application.AppId)' and createdDateTime ge $StartDate and signInEventTypes/any(t: t eq 'servicePrincipal')"

# Managed Identity
Get-MgBetaAuditLogSignIn -Filter "AppId eq '$($Application.AppId)' and createdDateTime ge $StartDate and signInEventTypes/any(t: t eq 'managedIdentity')"