{"id":155,"date":"2026-03-19T14:57:42","date_gmt":"2026-03-19T14:57:42","guid":{"rendered":"https:\/\/unen.nl\/?p=155"},"modified":"2026-03-19T14:57:52","modified_gmt":"2026-03-19T14:57:52","slug":"een-powershell-bootstrap-voor-modules-logging-en-minder-irritatie","status":"publish","type":"post","link":"https:\/\/unen.nl\/?p=155","title":{"rendered":"Een PowerShell bootstrap voor modules, logging en minder irritatie"},"content":{"rendered":"\n<p>Er zijn twee zekerheden in het leven:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Je drinkt koffie en die wordt koud.<\/li>\n\n\n\n<li>Op het moment dat je een PowerShell-script nodig hebt, ontbreekt er altijd n\u00e9t weer een module.<\/li>\n<\/ol>\n\n\n\n<p>En dat tweede begon me nogal te irriteren.<\/p>\n\n\n\n<p>Elke keer als ik een machine opnieuw had ingericht, een frisse sessie startte of weer op een andere omgeving werkte, begon hetzelfde toneelstuk opnieuw:<br><strong>module missen, module updaten, PSGallery corrigeren, repository fixen, transcript starten, logging regelen<\/strong>\u2026 en pas daarna kon ik eindelijk doen waarvoor ik PowerShell \u00fcberhaupt had geopend.<\/p>\n\n\n\n<p>Dus heb ik gedaan wat iedere licht gefrustreerde beheerder uiteindelijk doet:<br>ik heb er gewoon een script voor geschreven.<\/p>\n\n\n\n<p>Niet omdat het leuk was.<br>Nou ja, een beetje wel.<br>Maar vooral omdat ik het zat was om steeds handmatig hetzelfde circus op te tuigen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Waarom ik dit script heb gebouwd<\/h2>\n\n\n\n<p>Dit script is ontstaan uit pure praktische irritatie.<\/p>\n\n\n\n<p>Ik merkte dat ik bij mijn beheerwerkzaamheden steeds afhankelijk was van dezelfde set modules. En natuurlijk werkt het altijd zo dat precies die ene module ontbreekt op het moment dat je snel iets moet regelen.<\/p>\n\n\n\n<p>Dus in plaats van elke keer dit te doen:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>controleren of PSGallery nog leeft;<\/li>\n\n\n\n<li>kijken of NuGet nog meewerkt;<\/li>\n\n\n\n<li>modules handmatig installeren;<\/li>\n\n\n\n<li>modules bijwerken;<\/li>\n\n\n\n<li>transcript starten;<\/li>\n\n\n\n<li>logging regelen;<\/li>\n\n\n\n<li>hopen dat het deze keer w\u00e9l netjes loopt;<\/li>\n<\/ul>\n\n\n\n<p>\u2026 heb ik besloten dat PowerShell dat probleem zelf maar moest oplossen.<\/p>\n\n\n\n<p>Deze bootstrap wordt nu geladen zodra ik een specifiek ander script uitvoer. Met andere woorden: v\u00f3\u00f3rdat het echte werk begint, ruimt dit script eerst de rommel op. Een digitale versie van even de vaatwasser uitruimen voordat je gaat koken.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wat dit script precies doet<\/h2>\n\n\n\n<p>De nieuwste versie van mijn script doet meer dan alleen \u201ceven modules installeren\u201d.<\/p>\n\n\n\n<p>Het script:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>controleert logging en transcript;<\/li>\n\n\n\n<li>maakt benodigde mappen aan zoals <code>C:\\Temp<\/code>, <code>C:\\Tmp<\/code> en <code>C:\\Log<\/code>;<\/li>\n\n\n\n<li>configureert PowerShell Gallery en PSResourceGet;<\/li>\n\n\n\n<li>controleert of benodigde modules aanwezig zijn;<\/li>\n\n\n\n<li>installeert of werkt modules automatisch bij;<\/li>\n\n\n\n<li>ondersteunt zowel productie- als b\u00e8tamodules;<\/li>\n\n\n\n<li>kan modules direct importeren in de sessie;<\/li>\n\n\n\n<li>logt netjes welke gebruiker, machine en IP-adressen zijn gebruikt;<\/li>\n\n\n\n<li>kopieert het transcript zelfs naar de Intune-logmap als die aanwezig is.<\/li>\n<\/ul>\n\n\n\n<p>Oftewel: het is niet zomaar een installatiescript, maar eerder een kleine PowerShell-conci\u00ebrge die eerst alles netjes klaarzet voordat jij binnenkomt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wat er nieuw en beter is in deze versie<\/h2>\n\n\n\n<p>De update naar versie <strong>3.1.1<\/strong> is niet alleen cosmetisch. Hier zitten echt nuttige verbeteringen in.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Slimmere moduledefinities<\/h3>\n\n\n\n<p>De modules worden nu niet meer alleen als simpele namen verwerkt, maar ook als objecten en hashtables. Daardoor kan ik veel flexibeler werken met modulelijsten.<\/p>\n\n\n\n<p>Dat klinkt heel professioneel, maar komt eigenlijk neer op:<br>ik kan PowerShell nu eindelijk fatsoenlijk vertellen wat ik wil, zonder dat het script daar emotioneel van instort.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Ondersteuning voor pinned modules<\/h3>\n\n\n\n<p>Sommige modules wil je niet automatisch naar de nieuwste versie trekken. Soms heb je gewoon een specifieke versie nodig omdat \u201cnieuwer\u201d niet altijd hetzelfde is als \u201cbeter\u201d.<\/p>\n\n\n\n<p>Daarom kan ik nu modules vastzetten op een vereiste versie, zoals:<\/p>\n\n\n\n<p><code>CIS-M365-Benchmark<\/code> op versie <code>5.2.0<\/code><\/p>\n\n\n\n<p>Dat voorkomt verrassingen.<br>En met verrassingen bedoel ik natuurlijk: ellende.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Bootstrap-modules apart geregeld<\/h3>\n\n\n\n<p>De basis is nu opgesplitst in een eigen bootstrap-set met:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>PackageManagement<\/li>\n\n\n\n<li>PowerShellGet<\/li>\n\n\n\n<li>Microsoft.PowerShell.PSResourceGet<\/li>\n<\/ul>\n\n\n\n<p>Dat is slim, want als je afhankelijk bent van modulebeheer, moet je eerst zorgen dat je modulebeheer z\u00e9lf op orde is. Een beetje alsof je eerst je gereedschapskist repareert voordat je de schuurdeur gaat fiksen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Nettere fallback-logica<\/h3>\n\n\n\n<p>Als <code>Find-Module<\/code>, <code>Install-Module<\/code> of <code>Update-Module<\/code> niet meewerkt, probeert het script automatisch alternatieven via <code>PSResourceGet<\/code>.<\/p>\n\n\n\n<p>Oftewel:<br>als de voordeur dicht zit, probeert het script gewoon de achterdeur.<\/p>\n\n\n\n<p>Heel beschaafd overigens. Geen koevoet, gewoon nette PowerShell-logica.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Productie of b\u00e8ta? Allebei kan<\/h2>\n\n\n\n<p>Het script ondersteunt twee modi:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Production<\/h3>\n\n\n\n<p>Voor de dagelijkse, serieuze beheerwerkzaamheden met modules zoals:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Az.Accounts<\/li>\n\n\n\n<li>Microsoft.Graph<\/li>\n\n\n\n<li>Microsoft.Entra<\/li>\n\n\n\n<li>PnP.PowerShell<\/li>\n\n\n\n<li>ExchangeOnlineManagement<\/li>\n\n\n\n<li>MicrosoftTeams<\/li>\n\n\n\n<li>CIS-M365-Benchmark<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Beta<\/h3>\n\n\n\n<p>Voor de momenten waarop je denkt:<br><em>\u201cIk wil vooruitgang, spanning en een klein beetje risico.\u201d<\/em><\/p>\n\n\n\n<p>Dan kun je kiezen voor:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Az.Accounts<\/li>\n\n\n\n<li>Microsoft.Graph.Beta<\/li>\n\n\n\n<li>Microsoft.Entra.Beta<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Waarom dit voor mij prettig werkt<\/h2>\n\n\n\n<p>Wat ik zelf fijn vind aan deze aanpak, is dat ik er daarna niet meer over hoef na te denken.<\/p>\n\n\n\n<p>Ik start mijn andere script.<br>Deze bootstrap draait eerst.<br>Alle basisvoorwaarden worden gecontroleerd.<br>Modules worden bijgewerkt of ge\u00efnstalleerd.<br>Eventueel worden ze direct geladen.<br>En pas daarna begint het echte werk.<\/p>\n\n\n\n<p>Dat betekent dus minder:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u201cWaarom doet dit script niets?\u201d<\/li>\n\n\n\n<li>\u201cOh wacht, module ontbreekt.\u201d<\/li>\n\n\n\n<li>\u201cWaarom vertrouwt PSGallery zichzelf weer niet?\u201d<\/li>\n\n\n\n<li>\u201cWaarom ben ik alweer handmatig dingen aan het fixen?\u201d<\/li>\n<\/ul>\n\n\n\n<p>En meer:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>script starten;<\/li>\n\n\n\n<li>koffie pakken;<\/li>\n\n\n\n<li>doen waarvoor ik achter mijn laptop zat.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Het soort automatisering waar je echt blij van wordt<\/h2>\n\n\n\n<p>Niet alle automatisering hoeft groots, hip of AI-achtig te zijn.<\/p>\n\n\n\n<p>Soms is de beste automatisering gewoon:<br>iets bouwen dat jouw eigen irritatie oplost.<\/p>\n\n\n\n<p>Dat is precies wat dit script doet.<\/p>\n\n\n\n<p>Geen glamour.<br>Geen marketingpraat.<br>Gewoon een robuust PowerShell-script dat voorkomt dat ik iedere keer opnieuw dezelfde administratieve strafwerkzaamheden moet uitvoeren.<\/p>\n\n\n\n<p>En eerlijk?<br>Dat zijn vaak de fijnste scripts.<br>De scripts die niet indrukwekkend staan in een demo, maar die je op een drukke werkdag w\u00e9l redden van nodeloos gevloek.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Script<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"163\" src=\"http:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736-1024x163.png\" alt=\"\" class=\"wp-image-153\" srcset=\"https:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736-1024x163.png 1024w, https:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736-300x48.png 300w, https:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736-768x122.png 768w, https:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736-1536x244.png 1536w, https:\/\/unen.nl\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-18-114736.png 1681w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code>param(\n    &#91;switch]$Silent,\n    &#91;switch]$Beta,\n    &#91;switch]$ImportModules,\n    &#91;switch]$ForceInstall\n)\n\n&lt;#\n.NOTES\n============================================================================\n Created on:      2026-03-18\n Created by:      Vincent van Unen\n  Filename:        SCA_PowerShell-Modules.ps1\n============================================================================\n.DESCRIPTION\n Initialisatiescript voor MSP-beheerwerkzaamheden.\n\n Het script controleert:\n - logging en transcript\n - mappenstructuur\n - PSGallery- en PSResourceGet-configuratie\n - aanwezigheid van vereiste PowerShell-modules\n\n Standaard worden productiemodules verwerkt.\n Gebruik -Beta om uitsluitend b\u00e8tamodules te verwerken.\n.SYNOPSIS\n Initialiseert PowerShell-modules met logging-, transcript- en repositoryfunctionaliteit.\n.EXAMPLE\n .\\SCA_PowerShell-Modules.ps1\n\n Voert het script uit in productiemodus met standaardmodules.\n.EXAMPLE\n .\\SCA_PowerShell-Modules.ps1 -ImportModules\n\n Installeert of werkt modules bij en importeert deze direct in de sessie.\n.EXAMPLE\n .\\SCA_PowerShell-Modules.ps1 -Beta -ForceInstall\n\n Verwerkt uitsluitend b\u00e8tamodules en forceert installatie of update.\n#>\n\n#region Script metadata\n$ScriptAuthor        = 'Vincent van Unen'\n$ScriptVersion       = '3.1.1'\n$ScriptChangeDate    = '2026-03-19'\n$ScriptChangeLog     = 'Bugfix voor hashtable-moduledefinities en ondersteuning voor pinned modules, inclusief CIS-M365-Benchmark'\n$ScriptCurrentUser   = $env:UserName\n$ScriptRunningDevice = $env:COMPUTERNAME\n$CurrentDate         = Get-Date -Format 'yyyy-MM-dd'\n$LogName             = 'Initialize-SCA-Modules'\n$ScriptMode          = if ($Beta) { 'Beta' } else { 'Production' }\n#endregion Script metadata\n\n#region Security and session settings\ntry {\n    &#91;Net.ServicePointManager]::SecurityProtocol = &#91;Net.SecurityProtocolType]::Tls12\n} catch {}\n\ntry {\n    Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue\n    Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue\n} catch {}\n\n$MaximumFunctionCount = 16384\n$MaximumVariableCount = 16384\n#endregion Security and session settings\n\n#region Directories\n$TempDir1 = 'C:\\Temp'\n$TempDir2 = 'C:\\Tmp'\n$LogDir   = 'C:\\Log'\n\nforeach ($Folder in @($TempDir1, $TempDir2, $LogDir)) {\n    if (-not (Test-Path -Path $Folder)) {\n        New-Item -ItemType Directory -Path $Folder -Force | Out-Null\n    }\n}\n#endregion Directories\n\n#region Logging functions\nfunction Write-SCA_Status {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Message,\n\n        &#91;ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS', 'DEBUG')]\n        &#91;string]$Level = 'INFO'\n    )\n\n    if ($Silent) {\n        return\n    }\n\n    switch ($Level) {\n        'INFO'    { Write-Host \"&#91;INFO]    $Message\" -ForegroundColor Cyan }\n        'WARN'    { Write-Host \"&#91;WARN]    $Message\" -ForegroundColor Yellow }\n        'ERROR'   { Write-Host \"&#91;ERROR]   $Message\" -ForegroundColor Red }\n        'SUCCESS' { Write-Host \"&#91;OK]      $Message\" -ForegroundColor Green }\n        'DEBUG'   { Write-Host \"&#91;DEBUG]   $Message\" -ForegroundColor DarkGray }\n    }\n}\n\nfunction Write-SCA_LogFile {\n    &#91;CmdletBinding()]\n    param(\n        &#91;ValidateSet('INFO', 'WARN', 'ERROR', 'FATAL', 'DEBUG', 'SUCCESS')]\n        &#91;string]$Level = 'INFO',\n\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Message,\n\n        &#91;string]$LogFile = \"$LogDir\\$LogName-$CurrentDate.log\"\n    )\n\n    try {\n        if (-not (Test-Path -Path $LogFile)) {\n            New-Item -Path $LogFile -ItemType File -Force | Out-Null\n        }\n\n        if (&#91;string]::IsNullOrWhiteSpace($Message)) {\n            Add-Content -Path $LogFile -Value ''\n        }\n        else {\n            $DateStamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')\n            Add-Content -Path $LogFile -Value \"&#91;$DateStamp] &#91;$Level] $Message\"\n        }\n    }\n    catch {\n        Write-Host \"Log schrijven is mislukt: $($_.Exception.Message)\" -ForegroundColor Red\n    }\n}\n#endregion Logging functions\n\n#region Helper functions\nfunction Test-SCA_CommandExists {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name\n    )\n\n    return &#91;bool](Get-Command -Name $Name -ErrorAction SilentlyContinue)\n}\n\nfunction Get-SCA_PublicIpAddress {\n    &#91;CmdletBinding()]\n    param()\n\n    try {\n        return (Invoke-RestMethod -Uri 'https:\/\/ifconfig.me\/ip' -TimeoutSec 10)\n    }\n    catch {\n        return 'Onbekend'\n    }\n}\n\nfunction Get-SCA_PrivateIpAddress {\n    &#91;CmdletBinding()]\n    param()\n\n    try {\n        $Addresses = Get-NetIPAddress -ErrorAction Stop |\n            Where-Object {\n                $_.AddressFamily -eq 'IPv4' -and\n                $_.IPAddress -notlike '169.254*' -and\n                $_.IPAddress -ne '127.0.0.1'\n            } |\n            Select-Object -ExpandProperty IPAddress\n\n        if ($Addresses) {\n            return ($Addresses -join ', ')\n        }\n\n        return 'Onbekend'\n    }\n    catch {\n        return 'Onbekend'\n    }\n}\n\nfunction ConvertTo-SCA_ModuleObject {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;object]$Module\n    )\n\n    if ($Module -is &#91;string]) {\n        return &#91;pscustomobject]@{\n            Name            = $Module\n            RequiredVersion = $null\n        }\n    }\n\n    if ($Module -is &#91;hashtable]) {\n        if (-not $Module.ContainsKey('Name')) {\n            throw 'Ongeldige moduledefinitie: sleutel \"Name\" ontbreekt.'\n        }\n\n        return &#91;pscustomobject]@{\n            Name            = &#91;string]$Module&#91;'Name']\n            RequiredVersion = if ($Module.ContainsKey('RequiredVersion')) { &#91;string]$Module&#91;'RequiredVersion'] } else { $null }\n        }\n    }\n\n    if ($Module.PSObject.Properties.Name -contains 'Name') {\n        $RequiredVersion = $null\n\n        if ($Module.PSObject.Properties.Name -contains 'RequiredVersion') {\n            $RequiredVersion = &#91;string]$Module.RequiredVersion\n        }\n\n        return &#91;pscustomobject]@{\n            Name            = &#91;string]$Module.Name\n            RequiredVersion = $RequiredVersion\n        }\n    }\n\n    throw \"Ongeldige moduledefinitie: $($Module | Out-String)\"\n}\n\nfunction Initialize-SCA_PowerShellGet {\n    &#91;CmdletBinding()]\n    param()\n\n    if ($PSVersionTable.PSVersion.Major -ge 6) {\n        Write-SCA_Status -Message 'PowerShell 6+ gedetecteerd. Oude PowerShellGet-initialisatie wordt beperkt toegepast.' -Level DEBUG\n        Write-SCA_LogFile -Message 'PowerShell 6+ gedetecteerd. Oude PowerShellGet-initialisatie wordt beperkt toegepast.' -Level DEBUG\n    }\n\n    try {\n        $NuGetProvider = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue\n        if (-not $NuGetProvider) {\n            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ErrorAction Stop\n            Write-SCA_Status -Message 'NuGet-provider is ge\u00efnstalleerd.' -Level SUCCESS\n            Write-SCA_LogFile -Message 'NuGet-provider is ge\u00efnstalleerd.' -Level SUCCESS\n        }\n        else {\n            Write-SCA_Status -Message 'NuGet-provider is beschikbaar.' -Level SUCCESS\n            Write-SCA_LogFile -Message 'NuGet-provider is beschikbaar.' -Level SUCCESS\n        }\n    }\n    catch {\n        Write-SCA_Status -Message \"NuGet-provider kon niet worden ge\u00efnitialiseerd: $($_.Exception.Message)\" -Level ERROR\n        Write-SCA_LogFile -Message \"NuGet-provider kon niet worden ge\u00efnitialiseerd: $($_.Exception.Message)\" -Level ERROR\n        throw\n    }\n\n    try {\n        $InstalledPowerShellGet = Get-Module -ListAvailable -Name PowerShellGet |\n            Sort-Object Version -Descending |\n            Select-Object -First 1\n\n        if (-not $InstalledPowerShellGet -or $InstalledPowerShellGet.Version -lt &#91;version]'2.2.5') {\n            Install-Module -Name PowerShellGet -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop\n            Write-SCA_Status -Message 'PowerShellGet is ge\u00efnstalleerd of bijgewerkt.' -Level SUCCESS\n            Write-SCA_LogFile -Message 'PowerShellGet is ge\u00efnstalleerd of bijgewerkt.' -Level SUCCESS\n        }\n        else {\n            Write-SCA_Status -Message \"PowerShellGet is actueel. Versie: $($InstalledPowerShellGet.Version)\" -Level SUCCESS\n            Write-SCA_LogFile -Message \"PowerShellGet is actueel. Versie: $($InstalledPowerShellGet.Version)\" -Level SUCCESS\n        }\n    }\n    catch {\n        Write-SCA_Status -Message \"PowerShellGet kon niet worden bijgewerkt: $($_.Exception.Message)\" -Level WARN\n        Write-SCA_LogFile -Message \"PowerShellGet kon niet worden bijgewerkt: $($_.Exception.Message)\" -Level WARN\n    }\n}\n\nfunction Ensure-SCA_PowerShellGallery {\n    &#91;CmdletBinding()]\n    param()\n\n    Write-SCA_Status -Message 'PowerShell Gallery en package-management configureren...' -Level INFO\n    Write-SCA_LogFile -Message 'PowerShell Gallery en package-management configureren...' -Level INFO\n\n    try {\n        Initialize-SCA_PowerShellGet\n\n        try {\n            $PSGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue\n\n            if (-not $PSGallery) {\n                Register-PSRepository -Default -ErrorAction Stop\n                Write-SCA_Status -Message 'PSGallery is opnieuw geregistreerd.' -Level SUCCESS\n                Write-SCA_LogFile -Message 'PSGallery is opnieuw geregistreerd.' -Level SUCCESS\n                $PSGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction Stop\n            }\n\n            if ($PSGallery.InstallationPolicy -ne 'Trusted') {\n                Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -ErrorAction Stop\n                Write-SCA_Status -Message 'PSGallery is ingesteld op Trusted.' -Level SUCCESS\n                Write-SCA_LogFile -Message 'PSGallery is ingesteld op Trusted.' -Level SUCCESS\n            }\n            else {\n                Write-SCA_Status -Message 'PSGallery staat al op Trusted.' -Level SUCCESS\n                Write-SCA_LogFile -Message 'PSGallery staat al op Trusted.' -Level SUCCESS\n            }\n        }\n        catch {\n            Write-SCA_Status -Message \"PSGallery-configuratie via PowerShellGet is mislukt: $($_.Exception.Message)\" -Level WARN\n            Write-SCA_LogFile -Message \"PSGallery-configuratie via PowerShellGet is mislukt: $($_.Exception.Message)\" -Level WARN\n        }\n\n        if (Test-SCA_CommandExists -Name 'Register-PSResourceRepository') {\n            try {\n                $PSResourceRepo = Get-PSResourceRepository -Name 'PSGallery' -ErrorAction SilentlyContinue\n\n                if (-not $PSResourceRepo) {\n                    Register-PSResourceRepository -Name 'PSGallery' -Uri 'https:\/\/www.powershellgallery.com\/api\/v2' -Trusted -ErrorAction Stop\n                    Write-SCA_Status -Message 'PSResourceGet-repository PSGallery is geregistreerd.' -Level SUCCESS\n                    Write-SCA_LogFile -Message 'PSResourceGet-repository PSGallery is geregistreerd.' -Level SUCCESS\n                }\n                else {\n                    Write-SCA_Status -Message 'PSResourceGet-repository PSGallery is beschikbaar.' -Level SUCCESS\n                    Write-SCA_LogFile -Message 'PSResourceGet-repository PSGallery is beschikbaar.' -Level SUCCESS\n                }\n            }\n            catch {\n                Write-SCA_Status -Message \"PSResourceGet-repositoryconfiguratie is mislukt: $($_.Exception.Message)\" -Level WARN\n                Write-SCA_LogFile -Message \"PSResourceGet-repositoryconfiguratie is mislukt: $($_.Exception.Message)\" -Level WARN\n            }\n        }\n    }\n    catch {\n        Write-SCA_Status -Message \"Repositoryconfiguratie is mislukt: $($_.Exception.Message)\" -Level ERROR\n        Write-SCA_LogFile -Message \"Repositoryconfiguratie is mislukt: $($_.Exception.Message)\" -Level ERROR\n        throw\n    }\n}\n\nfunction Get-SCA_InstalledModuleVersion {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name\n    )\n\n    try {\n        if (Test-SCA_CommandExists -Name 'Get-InstalledPSResource') {\n            $InstalledPSResource = Get-InstalledPSResource -Name $Name -ErrorAction SilentlyContinue |\n                Sort-Object Version -Descending |\n                Select-Object -First 1\n\n            if ($InstalledPSResource) {\n                return &#91;pscustomobject]@{\n                    Name    = $InstalledPSResource.Name\n                    Version = $InstalledPSResource.Version\n                    Source  = 'PSResourceGet'\n                }\n            }\n        }\n\n        $InstalledModule = Get-InstalledModule -Name $Name -ErrorAction SilentlyContinue |\n            Sort-Object Version -Descending |\n            Select-Object -First 1\n\n        if ($InstalledModule) {\n            return &#91;pscustomobject]@{\n                Name    = $InstalledModule.Name\n                Version = $InstalledModule.Version\n                Source  = 'PowerShellGet'\n            }\n        }\n\n        return $null\n    }\n    catch {\n        return $null\n    }\n}\n\nfunction Get-SCA_GalleryModuleVersion {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name\n    )\n\n    try {\n        return Find-Module -Name $Name -Repository PSGallery -ErrorAction Stop\n    }\n    catch {\n        Write-SCA_Status -Message \"Find-Module kon $Name niet ophalen. Fallback via PSResourceGet wordt geprobeerd.\" -Level WARN\n        Write-SCA_LogFile -Message \"Find-Module kon $Name niet ophalen. Fallback via PSResourceGet wordt geprobeerd.\" -Level WARN\n\n        try {\n            if (Test-SCA_CommandExists -Name 'Find-PSResource') {\n                $PSResource = Find-PSResource -Name $Name -Repository PSGallery -ErrorAction Stop |\n                    Sort-Object Version -Descending |\n                    Select-Object -First 1\n\n                if ($PSResource) {\n                    return &#91;pscustomobject]@{\n                        Name    = $PSResource.Name\n                        Version = $PSResource.Version\n                    }\n                }\n            }\n        }\n        catch {\n            Write-SCA_Status -Message \"Fallback via PSResourceGet is mislukt voor $Name : $($_.Exception.Message)\" -Level WARN\n            Write-SCA_LogFile -Message \"Fallback via PSResourceGet is mislukt voor $Name : $($_.Exception.Message)\" -Level WARN\n        }\n\n        return $null\n    }\n}\n\nfunction Install-SCA_Module {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name,\n\n        &#91;string]$RequiredVersion\n    )\n\n    try {\n        $Params = @{\n            Name         = $Name\n            Repository   = 'PSGallery'\n            Scope        = 'CurrentUser'\n            Force        = $true\n            AllowClobber = $true\n            ErrorAction  = 'Stop'\n        }\n\n        if ($RequiredVersion) {\n            $Params.RequiredVersion = $RequiredVersion\n        }\n\n        Install-Module @Params\n        return $true\n    }\n    catch {\n        Write-SCA_Status -Message \"Install-Module is mislukt voor $Name. Fallback via Install-PSResource wordt geprobeerd.\" -Level WARN\n        Write-SCA_LogFile -Message \"Install-Module is mislukt voor $Name. Fallback via Install-PSResource wordt geprobeerd.\" -Level WARN\n\n        try {\n            if (Test-SCA_CommandExists -Name 'Install-PSResource') {\n                $PSResourceParams = @{\n                    Name            = $Name\n                    Repository      = 'PSGallery'\n                    Scope           = 'CurrentUser'\n                    TrustRepository = $true\n                    Force           = $true\n                    ErrorAction     = 'Stop'\n                }\n\n                if ($RequiredVersion) {\n                    $PSResourceParams.Version = $RequiredVersion\n                }\n\n                Install-PSResource @PSResourceParams\n                return $true\n            }\n        }\n        catch {\n            Write-SCA_Status -Message \"Install-PSResource is mislukt voor $Nam : $($_.Exception.Message)\" -Level ERROR\n            Write-SCA_LogFile -Message \"Install-PSResource is mislukt voor $Name : $($_.Exception.Message)\" -Level ERROR\n        }\n\n        return $false\n    }\n}\n\nfunction Update-SCA_Module {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name\n    )\n\n    try {\n        Update-Module -Name $Name -Force -ErrorAction Stop\n        return $true\n    }\n    catch {\n        Write-SCA_Status -Message \"Update-Module is mislukt voor $Name. Nieuwe installatie wordt geprobeerd.\" -Level WARN\n        Write-SCA_LogFile -Message \"Update-Module is mislukt voor $Name. Nieuwe installatie wordt geprobeerd.\" -Level WARN\n        return (Install-SCA_Module -Name $Name)\n    }\n}\n\nfunction InstallOrUpdate-SCA_Module {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;string]$Name,\n\n        &#91;string]$RequiredVersion,\n\n        &#91;switch]$ForceInstall\n    )\n\n    Write-SCA_Status -Message \"Module verwerken: $Name\" -Level INFO\n    Write-SCA_LogFile -Message \"Module verwerken: $Name\" -Level INFO\n\n    $Installed = Get-SCA_InstalledModuleVersion -Name $Name\n\n    if ($RequiredVersion) {\n        if (-not $Installed) {\n            Write-SCA_Status -Message \"$Name is niet ge\u00efnstalleerd. Installatie van vaste versie $RequiredVersion wordt gestart.\" -Level WARN\n            Write-SCA_LogFile -Message \"$Name is niet ge\u00efnstalleerd. Installatie van vaste versie $RequiredVersion wordt gestart.\" -Level WARN\n\n            if (Install-SCA_Module -Name $Name -RequiredVersion $RequiredVersion) {\n                Write-SCA_Status -Message \"$Name is succesvol ge\u00efnstalleerd op versie $RequiredVersion.\" -Level SUCCESS\n                Write-SCA_LogFile -Message \"$Name is succesvol ge\u00efnstalleerd op versie $RequiredVersion.\" -Level SUCCESS\n            }\n            else {\n                Write-SCA_Status -Message \"Installatie is mislukt voor $Name versie $RequiredVersion.\" -Level ERROR\n                Write-SCA_LogFile -Message \"Installatie is mislukt voor $Name versie $RequiredVersion.\" -Level ERROR\n            }\n\n            return\n        }\n\n        Write-SCA_Status -Message \"$Name lokaal: $($Installed.Version) | vereist: $RequiredVersion\" -Level INFO\n        Write-SCA_LogFile -Message \"$Name lokaal: $($Installed.Version) | vereist: $RequiredVersion\" -Level INFO\n\n        if ($ForceInstall -or (&#91;version]$Installed.Version -ne &#91;version]$RequiredVersion)) {\n            Write-SCA_Status -Message \"$Name wordt ge\u00efnstalleerd of gecorrigeerd naar versie $RequiredVersion.\" -Level WARN\n            Write-SCA_LogFile -Message \"$Name wordt ge\u00efnstalleerd of gecorrigeerd naar versie $RequiredVersion.\" -Level WARN\n\n            if (Install-SCA_Module -Name $Name -RequiredVersion $RequiredVersion) {\n                Write-SCA_Status -Message \"$Name is succesvol verwerkt op versie $RequiredVersion.\" -Level SUCCESS\n                Write-SCA_LogFile -Message \"$Name is succesvol verwerkt op versie $RequiredVersion.\" -Level SUCCESS\n            }\n            else {\n                Write-SCA_Status -Message \"Installatie is mislukt voor $Name versie $RequiredVersion.\" -Level ERROR\n                Write-SCA_LogFile -Message \"Installatie is mislukt voor $Name versie $RequiredVersion.\" -Level ERROR\n            }\n        }\n        else {\n            Write-SCA_Status -Message \"$Name staat al op de vereiste versie $RequiredVersion.\" -Level SUCCESS\n            Write-SCA_LogFile -Message \"$Name staat al op de vereiste versie $RequiredVersion.\" -Level SUCCESS\n        }\n\n        return\n    }\n\n    $Gallery = Get-SCA_GalleryModuleVersion -Name $Name\n\n    if (-not $Gallery) {\n        Write-SCA_Status -Message \"Module $Name is niet gevonden in PSGallery of kon niet worden uitgelezen.\" -Level ERROR\n        Write-SCA_LogFile -Message \"Module $Name is niet gevonden in PSGallery of kon niet worden uitgelezen.\" -Level ERROR\n        return\n    }\n\n    if (-not $Installed) {\n        Write-SCA_Status -Message \"$Name is niet ge\u00efnstalleerd. Installatie wordt gestart.\" -Level WARN\n        Write-SCA_LogFile -Message \"$Name is niet ge\u00efnstalleerd. Installatie wordt gestart.\" -Level WARN\n\n        if (Install-SCA_Module -Name $Name) {\n            Write-SCA_Status -Message \"$Name is succesvol ge\u00efnstalleerd. Versie: $($Gallery.Version)\" -Level SUCCESS\n            Write-SCA_LogFile -Message \"$Name is succesvol ge\u00efnstalleerd. Versie: $($Gallery.Version)\" -Level SUCCESS\n        }\n        else {\n            Write-SCA_Status -Message \"Installatie is mislukt voor $Name.\" -Level ERROR\n            Write-SCA_LogFile -Message \"Installatie is mislukt voor $Name.\" -Level ERROR\n        }\n\n        return\n    }\n\n    Write-SCA_Status -Message \"$Name lokaal: $($Installed.Version) | gallery: $($Gallery.Version)\" -Level INFO\n    Write-SCA_LogFile -Message \"$Name lokaal: $($Installed.Version) | gallery: $($Gallery.Version)\" -Level INFO\n\n    if ($ForceInstall -or (&#91;version]$Gallery.Version -gt &#91;version]$Installed.Version)) {\n        Write-SCA_Status -Message \"$Name wordt bijgewerkt.\" -Level WARN\n        Write-SCA_LogFile -Message \"$Name wordt bijgewerkt.\" -Level WARN\n\n        if (Update-SCA_Module -Name $Name) {\n            Write-SCA_Status -Message \"$Name is bijgewerkt naar versie $($Gallery.Version).\" -Level SUCCESS\n            Write-SCA_LogFile -Message \"$Name is bijgewerkt naar versie $($Gallery.Version).\" -Level SUCCESS\n        }\n        else {\n            Write-SCA_Status -Message \"Bijwerken is mislukt voor $Name.\" -Level ERROR\n            Write-SCA_LogFile -Message \"Bijwerken is mislukt voor $Name.\" -Level ERROR\n        }\n    }\n    else {\n        Write-SCA_Status -Message \"$Name is al actueel.\" -Level SUCCESS\n        Write-SCA_LogFile -Message \"$Name is al actueel.\" -Level SUCCESS\n    }\n}\n\nfunction Import-SCA_SelectedModules {\n    &#91;CmdletBinding()]\n    param(\n        &#91;Parameter(Mandatory = $true)]\n        &#91;object&#91;]]$Modules\n    )\n\n    foreach ($ModuleDefinition in $Modules) {\n        $Module = ConvertTo-SCA_ModuleObject -Module $ModuleDefinition\n\n        try {\n            Import-Module $Module.Name -Force -ErrorAction Stop\n            Write-SCA_Status -Message \"$($Module.Name) is geladen in de sessie.\" -Level SUCCESS\n            Write-SCA_LogFile -Message \"$($Module.Name) is geladen in de sessie.\" -Level SUCCESS\n        }\n        catch {\n            Write-SCA_Status -Message \"Importeren is mislukt voor $($Module.Name): $($_.Exception.Message)\" -Level ERROR\n            Write-SCA_LogFile -Message \"Importeren is mislukt voor $($Module.Name): $($_.Exception.Message)\" -Level ERROR\n        }\n    }\n}\n#endregion Helper functions\n\n#region Module configuration\n$ModuleSets = @{\n    Bootstrap = @(\n        @{ Name = 'PackageManagement' },\n        @{ Name = 'PowerShellGet' },\n        @{ Name = 'Microsoft.PowerShell.PSResourceGet' }\n    )\n\n    Production = @(\n        @{ Name = 'Az.Accounts' },\n        @{ Name = 'Microsoft.Graph' },\n        @{ Name = 'Microsoft.Entra' },\n        @{ Name = 'PnP.PowerShell' },\n        @{ Name = 'Microsoft.Online.SharePoint.PowerShell' },\n        @{ Name = 'ExchangeOnlineManagement' },\n        @{ Name = 'MicrosoftTeams' },\n        @{ Name = 'CIS-M365-Benchmark'; RequiredVersion = '5.2.0' }\n    )\n\n    Beta = @(\n        @{ Name = 'Az.Accounts' },\n        @{ Name = 'Microsoft.Graph.Beta' },\n        @{ Name = 'Microsoft.Entra.Beta' }\n    )\n}\n\n$SelectedModules = if ($Beta) {\n    $ModuleSets.Beta\n}\nelse {\n    $ModuleSets.Production\n}\n\n$AllModules = @($ModuleSets.Bootstrap + $SelectedModules)\n#endregion Module configuration\n\n#region Logging header\n$PublicIP  = Get-SCA_PublicIpAddress\n$PrivateIP = Get-SCA_PrivateIpAddress\n\nWrite-SCA_LogFile -Message \"Current Date = $CurrentDate\"\nWrite-SCA_LogFile -Message \"Script Author = $ScriptAuthor\"\nWrite-SCA_LogFile -Message \"Script Version = $ScriptVersion\"\nWrite-SCA_LogFile -Message \"Script ChangeDate = $ScriptChangeDate\"\nWrite-SCA_LogFile -Message \"Script ChangeLog = $ScriptChangeLog\"\nWrite-SCA_LogFile -Message \"Current User Running this script = $ScriptCurrentUser\"\nWrite-SCA_LogFile -Message \"Current Device Running this script = $ScriptRunningDevice\"\nWrite-SCA_LogFile -Message \"Current Public IP = $PublicIP\"\nWrite-SCA_LogFile -Message \"Current Private IP = $PrivateIP\"\nWrite-SCA_LogFile -Message \"Script mode = $ScriptMode\"\nWrite-SCA_LogFile -Message \"Bootstrap modules = $((($ModuleSets.Bootstrap | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))\"\nWrite-SCA_LogFile -Message \"Selected modules = $((($SelectedModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))\"\nWrite-SCA_LogFile -Message \"All modules = $((($AllModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))\"\n\nWrite-SCA_Status -Message \"Scriptmodus: $ScriptMode\" -Level INFO\nWrite-SCA_Status -Message \"Bootstrapmodules: $((($ModuleSets.Bootstrap | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))\" -Level INFO\nWrite-SCA_Status -Message \"Geselecteerde modules: $((($SelectedModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))\" -Level INFO\n#endregion Logging header\n\n#region Transcript\n$TranscriptFile    = \"$LogDir\\$CurrentDate-$LogName-Transcript.log\"\n$TranscriptStarted = $false\n\ntry {\n    Start-Transcript -Path $TranscriptFile -Force | Out-Null\n    $TranscriptStarted = $true\n    Write-SCA_LogFile -Message \"Transcript gestart: $TranscriptFile\" -Level SUCCESS\n}\ncatch {\n    Write-SCA_LogFile -Message \"Start-Transcript is mislukt: $($_.Exception.Message)\" -Level ERROR\n}\n#endregion Transcript\n\n#region Main\ntry {\n    Ensure-SCA_PowerShellGallery\n\n    foreach ($ModuleDefinition in $AllModules) {\n        $Module = ConvertTo-SCA_ModuleObject -Module $ModuleDefinition\n\n        InstallOrUpdate-SCA_Module `\n            -Name $Module.Name `\n            -RequiredVersion $Module.RequiredVersion `\n            -ForceInstall:$ForceInstall\n    }\n\n    if ($ImportModules) {\n        Import-SCA_SelectedModules -Modules $SelectedModules\n    }\n\n    Write-SCA_Status -Message \"Module-initialisatie is afgerond voor modus: $ScriptMode\" -Level SUCCESS\n    Write-SCA_LogFile -Message \"Module-initialisatie is afgerond voor modus: $ScriptMode\" -Level SUCCESS\n}\ncatch {\n    Write-SCA_Status -Message \"Het script is be\u00ebindigd met een fout: $($_.Exception.Message)\" -Level ERROR\n    Write-SCA_LogFile -Message \"Het script is be\u00ebindigd met een fout: $($_.Exception.Message)\" -Level FATAL\n}\nfinally {\n    if ($TranscriptStarted) {\n        try {\n            Stop-Transcript | Out-Null\n            Write-SCA_LogFile -Message 'Transcript gestopt.' -Level SUCCESS\n        }\n        catch {\n            Write-SCA_LogFile -Message \"Stop-Transcript is mislukt: $($_.Exception.Message)\" -Level ERROR\n        }\n    }\n\n    $IntuneLogPath = 'C:\\ProgramData\\Microsoft\\IntuneManagementExtension\\Logs'\n\n    if ((Test-Path -Path $IntuneLogPath) -and (Test-Path -Path $TranscriptFile)) {\n        try {\n            Copy-Item -Path $TranscriptFile -Destination $IntuneLogPath -Force -ErrorAction Stop\n            Write-SCA_LogFile -Message 'Transcript is gekopieerd naar de Intune-logmap.' -Level SUCCESS\n        }\n        catch {\n            Write-SCA_LogFile -Message \"Kopi\u00ebren naar de Intune-logmap is mislukt: $($_.Exception.Message)\" -Level ERROR\n        }\n    }\n}\n#endregion Main<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusie<\/h2>\n\n\n\n<p>Dit script heb ik geschreven omdat ik het zat was om handmatig modules te installeren, te updaten en te herstellen als ik weer een machine opnieuw had ingericht of in een verse omgeving werkte.<\/p>\n\n\n\n<p>Dus heb ik het proces geautomatiseerd.<\/p>\n\n\n\n<p>Nu draait dit script telkens mee wanneer ik een ander specifiek script uitvoer, zodat mijn PowerShell-omgeving eerst netjes wordt klaargezet. Geen losse eindjes, geen ontbrekende modules en een stuk minder frustratie.<\/p>\n\n\n\n<p>Kort gezegd:<br>ik heb een terugkerend probleem vervangen door een script.<\/p>\n\n\n\n<p>Zoals het hoort.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Er zijn twee zekerheden in het leven: En dat tweede begon me nogal te irriteren. Elke keer als<\/p>\n","protected":false},"author":2,"featured_media":151,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[],"class_list":["post-155","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-powershell"],"_links":{"self":[{"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/posts\/155","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/unen.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=155"}],"version-history":[{"count":2,"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/posts\/155\/revisions"}],"predecessor-version":[{"id":157,"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/posts\/155\/revisions\/157"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/unen.nl\/index.php?rest_route=\/wp\/v2\/media\/151"}],"wp:attachment":[{"href":"https:\/\/unen.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=155"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/unen.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=155"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/unen.nl\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=155"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}