Er zijn van die dingen in het leven waarvan je denkt: “Dit ga ik de volgende keer echt beter doen.”
Bij mij was dat… PowerShell modules.
Want laten we eerlijk zijn:
- Nieuwe machine? → modules kwijt
- Nieuwe tenant? → modules missen
- Script draaien? → “Module not found” 😑
- En dan begint het feest weer: installeren, updaten, fouten fixen…
Na de zoveelste keer dacht ik: dit kan slimmer.
(Spoiler: dat dacht ik de vorige 10 keer ook.)
Het probleem (aka mijn frustratie)
Ik merkte dat ik steeds hetzelfde aan het doen was:
- Modules installeren
- Modules updaten
- PSGallery fixen (want die is altijd stuk wanneer je hem nodig hebt)
- Importeren in de sessie
- En hopen dat alles werkt voordat ik mijn echte werk kan doen
En dit gebeurde elke keer opnieuw.
Alsof ik een soort PowerShell versie van Groundhog Day leefde.
De oplossing: een “bootstrap script”
Dus heb ik een script gemaakt dat:
- Automatisch mijn modules installeert of bijwerkt
- PSGallery en NuGet fixt (ja, ook als het stuk is)
- Logging en transcript netjes regelt
- Modules kan importeren
- Zowel production als beta ondersteunt
- En gewoon elke keer draait voordat mijn echte scripts starten
Met andere woorden:
👉 ik heb mijn toekomstige frustratie geautomatiseerd weggewerkt
Hoe ik het gebruik
Dit script wordt bij mij altijd eerst geladen wanneer ik een ander script start.
Dus in plaats van:
“Waarom werkt dit niet?!”
wordt het nu:
“Oh ja… modules zijn al geregeld.”
En geloof me: dat scheelt een hoop mentale schade.
Wat dit script allemaal doet (kort en krachtig)
- Zet TLS en execution policy goed
- Maakt log- en tempmappen aan
- Zet PSGallery op “Trusted” (want ja… dat blijft een ding)
- Checkt en installeert NuGet / PowerShellGet
- Vergelijkt lokale vs gallery versies
- Installeert of update modules slim
- Fallback naar PSResourceGet als PowerShellGet faalt
- Logging + transcript (inclusief Intune copy 👌)

De modules (want daar ging het allemaal om)
Production:
- Microsoft.Graph
- Microsoft.Entra
- PnP.PowerShell
- ExchangeOnlineManagement
- MicrosoftTeams
- CIS-M365-Benchmark
- etc.
Beta (voor de waaghalzen):
- Microsoft.Graph.Beta
- Microsoft.Entra.Beta
Het script
En ja… dit is hem.
De reden dat ik nooit meer handmatig modules hoef te fixen:
param(
[switch]$Silent,
[switch]$Beta,
[switch]$ImportModules,
[switch]$ForceInstall
)
<#
.NOTES
===========================================================================
Created on: 2026-03-18
Created by: Vincent van Unen
Organization: Xiphos SCALE IT
Filename: SCA_Powershell-Modules.ps1
===========================================================================
.DESCRIPTION
Initialisatiescript voor MSP-beheerwerkzaamheden.
Controleert logging, transcript, mappenstructuur, PSGallery-configuratie
en de aanwezigheid van vereiste PowerShell-modules.
Standaard worden productiemodules geladen.
Gebruik -Beta om bètamodules te laden.
.SYNOPSIS
Initialiseert PowerShell-modules met logging- en transcriptfunctionaliteit.
.EXAMPLE
.\SCA_Powershell-Modules.ps1
Voert het script uit in productiemodus met standaardmodules.
.EXAMPLE
.\SCA_Powershell-Modules.ps1 -ImportModules
Installeert of werkt modules bij en importeert deze direct in de sessie.
.EXAMPLE
.\SCA_Powershell-Modules.ps1 -Beta -ForceInstall
Verwerkt uitsluitend bètamodules en forceert installatie of update.
#>
#region Script metadata
$ScriptAuthor = "Vincent van Unen"
$ScriptVersion = "2.0.0"
$ScriptChangeDate = "2026-03-18"
$ScriptChangeLog = "Volledig vernieuwde module-bootstrap met PSGallery-herstel, PSResourceGet-fallback en CIS-M365-Benchmark-ondersteuning"
$ScriptCurrentUser = $env:UserName
$ScriptRunningDevice = $env:COMPUTERNAME
$CurrentDate = Get-Date -Format 'yyyy-MM-dd'
$LogName = "Initialize-XIPModules"
$ScriptMode = if ($Beta) { "Beta" } else { "Production" }
#endregion Script metadata
#region Security and session settings
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
} catch {}
try {
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
} catch {}
$MaximumFunctionCount = 16384
$MaximumVariableCount = 16384
#endregion Security and session settings
#region Directories
$TempDir1 = "C:\Temp"
$TempDir2 = "C:\Tmp"
$LogDir = "C:\Log"
foreach ($Folder in @($TempDir1, $TempDir2, $LogDir)) {
if (-not (Test-Path -Path $Folder)) {
New-Item -ItemType Directory -Path $Folder -Force | Out-Null
}
}
#endregion Directories
#region Logging functions
function Write-SCA_Status {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet('INFO','WARN','ERROR','SUCCESS','DEBUG')]
[string]$Level = 'INFO'
)
if ($Silent) {
return
}
switch ($Level) {
'INFO' { Write-Host "[INFO] $Message" -ForegroundColor Cyan }
'WARN' { Write-Host "[WARN] $Message" -ForegroundColor Yellow }
'ERROR' { Write-Host "[ERROR] $Message" -ForegroundColor Red }
'SUCCESS' { Write-Host "[OK] $Message" -ForegroundColor Green }
'DEBUG' { Write-Host "[DEBUG] $Message" -ForegroundColor DarkGray }
}
}
function Write-SCA_LogFile {
[CmdletBinding()]
param(
[ValidateSet("INFO","WARN","ERROR","FATAL","DEBUG","SUCCESS")]
[string]$Level = "INFO",
[Parameter(Mandatory = $true)]
[string]$Message,
[string]$LogFile = "$LogDir\$LogName-$CurrentDate.log"
)
try {
if (-not (Test-Path -Path $LogFile)) {
New-Item -Path $LogFile -ItemType File -Force | Out-Null
}
if ([string]::IsNullOrWhiteSpace($Message)) {
Add-Content -Path $LogFile -Value ""
}
else {
$DateStamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
Add-Content -Path $LogFile -Value "[$DateStamp] [$Level] $Message"
}
}
catch {
Write-Host "Log schrijven mislukt: $($_.Exception.Message)" -ForegroundColor Red
}
}
#endregion Logging functions
#region Helper functions
function Get-SCA_PublicIpAddress {
[CmdletBinding()]
param()
try {
return (Invoke-RestMethod -Uri "https://ifconfig.me/ip" -TimeoutSec 10)
}
catch {
return "Onbekend"
}
}
function Get-SCA_PrivateIpAddress {
[CmdletBinding()]
param()
try {
$Addresses = Get-NetIPAddress -ErrorAction Stop |
Where-Object {
$_.AddressFamily -eq 'IPv4' -and
$_.IPAddress -notlike '169.254*' -and
$_.IPAddress -ne '127.0.0.1'
} |
Select-Object -ExpandProperty IPAddress
if ($Addresses) {
return ($Addresses -join ', ')
}
return "Onbekend"
}
catch {
return "Onbekend"
}
}
function Initialize-SCA_PowerShellGet {
[CmdletBinding()]
param()
if ($PSVersionTable.PSVersion.Major -ge 6) {
return
}
try {
$NuGetProvider = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
if (-not $NuGetProvider) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ErrorAction Stop
Write-SCA_Status -Message "NuGet-provider is geïnstalleerd." -Level SUCCESS
Write-SCA_LogFile -Message "NuGet-provider is geïnstalleerd." -Level SUCCESS
}
}
catch {
Write-SCA_Status -Message "NuGet-provider kon niet worden geïnstalleerd: $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "NuGet-provider kon niet worden geïnstalleerd: $($_.Exception.Message)" -Level ERROR
throw
}
try {
$InstalledPowerShellGet = Get-Module -ListAvailable -Name PowerShellGet |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $InstalledPowerShellGet -or $InstalledPowerShellGet.Version -lt [version]'2.2.5') {
Install-Module -Name PowerShellGet -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
Write-SCA_Status -Message "PowerShellGet is bijgewerkt." -Level SUCCESS
Write-SCA_LogFile -Message "PowerShellGet is bijgewerkt." -Level SUCCESS
}
else {
Write-SCA_Status -Message "PowerShellGet is actueel." -Level SUCCESS
Write-SCA_LogFile -Message "PowerShellGet is actueel." -Level SUCCESS
}
}
catch {
Write-SCA_Status -Message "PowerShellGet kon niet worden bijgewerkt: $($_.Exception.Message)" -Level WARN
Write-SCA_LogFile -Message "PowerShellGet kon niet worden bijgewerkt: $($_.Exception.Message)" -Level WARN
}
}
function Ensure-SCA_PowerShellGallery {
[CmdletBinding()]
param()
Write-SCA_Status -Message "PowerShell Gallery en NuGet configureren..." -Level INFO
Write-SCA_LogFile -Message "PowerShell Gallery en NuGet configureren..." -Level INFO
try {
Initialize-SCA_PowerShellGet
# --- NuGet provider afdwingen ---
try {
$NuGetProvider = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
if (-not $NuGetProvider) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ErrorAction Stop
Write-SCA_Status -Message "NuGet-provider is geïnstalleerd." -Level SUCCESS
Write-SCA_LogFile -Message "NuGet-provider is geïnstalleerd." -Level SUCCESS
}
else {
Write-SCA_Status -Message "NuGet-provider is beschikbaar." -Level SUCCESS
Write-SCA_LogFile -Message "NuGet-provider is beschikbaar." -Level SUCCESS
}
}
catch {
Write-SCA_Status -Message "NuGet-provider kon niet worden geïnstalleerd: $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "NuGet-provider kon niet worden geïnstalleerd: $($_.Exception.Message)" -Level ERROR
throw
}
# --- PSGallery controleren ---
$PSGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue
if (-not $PSGallery) {
Register-PSRepository -Default -ErrorAction Stop
Write-SCA_Status -Message "PSGallery is opnieuw geregistreerd." -Level SUCCESS
Write-SCA_LogFile -Message "PSGallery is opnieuw geregistreerd." -Level SUCCESS
$PSGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction Stop
}
if ($PSGallery.InstallationPolicy -ne 'Trusted') {
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -ErrorAction Stop
Write-SCA_Status -Message "PSGallery is ingesteld op Trusted." -Level SUCCESS
Write-SCA_LogFile -Message "PSGallery is ingesteld op Trusted." -Level SUCCESS
}
else {
Write-SCA_Status -Message "PSGallery staat al op Trusted." -Level SUCCESS
Write-SCA_LogFile -Message "PSGallery staat al op Trusted." -Level SUCCESS
}
# --- PSResourceGet repository check (moderne stack) ---
if (Get-Command -Name Register-PSResourceRepository -ErrorAction SilentlyContinue) {
$Repo = Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue
if (-not $Repo) {
Register-PSResourceRepository -Name PSGallery -Uri "https://www.powershellgallery.com/api/v2" -Trusted -ErrorAction Stop
Write-SCA_Status -Message "PSResourceGet PSGallery repository geregistreerd." -Level SUCCESS
Write-SCA_LogFile -Message "PSResourceGet PSGallery repository geregistreerd." -Level SUCCESS
}
}
}
catch {
Write-SCA_Status -Message "Repositoryconfiguratie mislukt: $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "Repositoryconfiguratie mislukt: $($_.Exception.Message)" -Level ERROR
throw
}
}
function Get-SCA_InstalledModuleVersion {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
try {
return Get-InstalledModule -Name $Name -ErrorAction SilentlyContinue |
Sort-Object Version -Descending |
Select-Object -First 1
}
catch {
return $null
}
}
function Get-SCA_GalleryModuleVersion {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
try {
return Find-Module -Name $Name -Repository PSGallery -ErrorAction Stop
}
catch {
Write-SCA_Status -Message "Find-Module kon $Name niet ophalen. Fallback via PSResourceGet wordt geprobeerd." -Level WARN
Write-SCA_LogFile -Message "Find-Module kon $Name niet ophalen. Fallback via PSResourceGet wordt geprobeerd." -Level WARN
try {
if (Get-Command -Name Find-PSResource -ErrorAction SilentlyContinue) {
$PSResource = Find-PSResource -Name $Name -Repository PSGallery -ErrorAction Stop |
Sort-Object Version -Descending |
Select-Object -First 1
if ($PSResource) {
return [pscustomobject]@{
Name = $PSResource.Name
Version = $PSResource.Version
}
}
}
}
catch {
Write-SCA_Status -Message "Fallback via PSResourceGet is mislukt voor $Name : $($_.Exception.Message)" -Level WARN
Write-SCA_LogFile -Message "Fallback via PSResourceGet is mislukt voor $Name : $($_.Exception.Message)" -Level WARN
}
return $null
}
}
function Install-SCA_Module {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
try {
Install-Module -Name $Name -Repository PSGallery -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
return $true
}
catch {
Write-SCA_Status -Message "Install-Module is mislukt voor $Name. Fallback via Install-PSResource wordt geprobeerd." -Level WARN
Write-SCA_LogFile -Message "Install-Module is mislukt voor $Name. Fallback via Install-PSResource wordt geprobeerd." -Level WARN
try {
if (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue) {
Install-PSResource -Name $Name -Repository PSGallery -Scope CurrentUser -TrustRepository -Force -ErrorAction Stop
return $true
}
}
catch {
Write-SCA_Status -Message "Install-PSResource is mislukt voor $Name : $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "Install-PSResource is mislukt voor $Name : $($_.Exception.Message)" -Level ERROR
}
return $false
}
}
function Update-SCA_Module {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
try {
Update-Module -Name $Name -Force -ErrorAction Stop
return $true
}
catch {
Write-SCA_Status -Message "Update-Module is mislukt voor $Name. Installatie wordt opnieuw geprobeerd." -Level WARN
Write-SCA_LogFile -Message "Update-Module is mislukt voor $Name. Installatie wordt opnieuw geprobeerd." -Level WARN
return (Install-SCA_Module -Name $Name)
}
}
function InstallOrUpdate-SCA_Module {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[switch]$ForceInstall
)
Write-SCA_Status -Message "Module verwerken: $Name" -Level INFO
Write-SCA_LogFile -Message "Module verwerken: $Name" -Level INFO
$Installed = Get-SCA_InstalledModuleVersion -Name $Name
$Gallery = Get-SCA_GalleryModuleVersion -Name $Name
if (-not $Gallery) {
Write-SCA_Status -Message "Module $Name is niet gevonden in PSGallery of kon niet worden uitgelezen." -Level ERROR
Write-SCA_LogFile -Message "Module $Name is niet gevonden in PSGallery of kon niet worden uitgelezen." -Level ERROR
return
}
if (-not $Installed) {
Write-SCA_Status -Message "$Name is niet geïnstalleerd. Installatie wordt gestart." -Level WARN
Write-SCA_LogFile -Message "$Name is niet geïnstalleerd. Installatie wordt gestart." -Level WARN
if (Install-SCA_Module -Name $Name) {
Write-SCA_Status -Message "$Name is succesvol geïnstalleerd. Versie: $($Gallery.Version)" -Level SUCCESS
Write-SCA_LogFile -Message "$Name is succesvol geïnstalleerd. Versie: $($Gallery.Version)" -Level SUCCESS
}
else {
Write-SCA_Status -Message "Installatie is mislukt voor $Name." -Level ERROR
Write-SCA_LogFile -Message "Installatie is mislukt voor $Name." -Level ERROR
}
return
}
Write-SCA_Status -Message "$Name lokaal: $($Installed.Version) | gallery: $($Gallery.Version)" -Level INFO
Write-SCA_LogFile -Message "$Name lokaal: $($Installed.Version) | gallery: $($Gallery.Version)" -Level INFO
if ($ForceInstall -or ([version]$Gallery.Version -gt [version]$Installed.Version)) {
Write-SCA_Status -Message "$Name wordt bijgewerkt." -Level WARN
Write-SCA_LogFile -Message "$Name wordt bijgewerkt." -Level WARN
if (Update-SCA_Module -Name $Name) {
Write-SCA_Status -Message "$Name is bijgewerkt naar versie $($Gallery.Version)." -Level SUCCESS
Write-SCA_LogFile -Message "$Name is bijgewerkt naar versie $($Gallery.Version)." -Level SUCCESS
}
else {
Write-SCA_Status -Message "Bijwerken is mislukt voor $Name." -Level ERROR
Write-SCA_LogFile -Message "Bijwerken is mislukt voor $Name." -Level ERROR
}
}
else {
Write-SCA_Status -Message "$Name is al actueel." -Level SUCCESS
Write-SCA_LogFile -Message "$Name is al actueel." -Level SUCCESS
}
}
function Import-SCA_SelectedModules {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$Modules
)
foreach ($Module in $Modules) {
try {
Import-Module $Module -Force -ErrorAction Stop
Write-SCA_Status -Message "$Module is geladen in de sessie." -Level SUCCESS
Write-SCA_LogFile -Message "$Module is geladen in de sessie." -Level SUCCESS
}
catch {
Write-SCA_Status -Message "Importeren is mislukt voor $Module : $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "Importeren is mislukt voor $Module : $($_.Exception.Message)" -Level ERROR
}
}
}
#endregion Helper functions
#region Module configuration
$ModuleSets = @{
Production = @(
'Microsoft.Graph',
'Microsoft.Entra',
'PnP.PowerShell',
'Microsoft.Online.SharePoint.PowerShell',
'ExchangeOnlineManagement',
'MicrosoftTeams',
'CIS-M365-Benchmark'
)
Beta = @(
'Microsoft.Graph.Beta',
'Microsoft.Entra.Beta'
)
}
$SelectedModules = if ($Beta) {
$ModuleSets.Beta
}
else {
$ModuleSets.Production
}
#endregion Module configuration
#region Logging header
$PublicIP = Get-SCA_PublicIpAddress
$PrivateIP = Get-SCA_PrivateIpAddress
Write-SCA_LogFile -Message "Current Date = $CurrentDate"
Write-SCA_LogFile -Message "Script Author = $ScriptAuthor"
Write-SCA_LogFile -Message "Script Version = $ScriptVersion"
Write-SCA_LogFile -Message "Script ChangeDate = $ScriptChangeDate"
Write-SCA_LogFile -Message "Script ChangeLog = $ScriptChangeLog"
Write-SCA_LogFile -Message "Current User Running this script = $ScriptCurrentUser"
Write-SCA_LogFile -Message "Current Device Running this script = $ScriptRunningDevice"
Write-SCA_LogFile -Message "Current Public IP = $PublicIP"
Write-SCA_LogFile -Message "Current Private IP = $PrivateIP"
Write-SCA_LogFile -Message "Script mode = $ScriptMode"
Write-SCA_LogFile -Message "Selected modules = $($SelectedModules -join ', ')"
Write-SCA_Status -Message "Scriptmodus: $ScriptMode" -Level INFO
Write-SCA_Status -Message "Geselecteerde modules: $($SelectedModules -join ', ')" -Level INFO
#endregion Logging header
#region Transcript
$TranscriptFile = "$LogDir\$CurrentDate-$LogName-Transcript.log"
$TranscriptStarted = $false
try {
Start-Transcript -Path $TranscriptFile -Force | Out-Null
$TranscriptStarted = $true
Write-SCA_LogFile -Message "Transcript gestart: $TranscriptFile" -Level SUCCESS
}
catch {
Write-SCA_LogFile -Message "Start-Transcript is mislukt: $($_.Exception.Message)" -Level ERROR
}
#endregion Transcript
#region Main
try {
Ensure-SCA_PowerShellGallery
foreach ($Module in $SelectedModules) {
InstallOrUpdate-SCA_Module -Name $Module -ForceInstall:$ForceInstall
}
if ($ImportModules) {
Import-SCA_SelectedModules -Modules $SelectedModules
}
Write-SCA_Status -Message "Module-initialisatie is afgerond voor modus: $ScriptMode" -Level SUCCESS
Write-SCA_LogFile -Message "Module-initialisatie is afgerond voor modus: $ScriptMode" -Level SUCCESS
}
catch {
Write-SCA_Status -Message "Script is beëindigd met een fout: $($_.Exception.Message)" -Level ERROR
Write-SCA_LogFile -Message "Script is beëindigd met een fout: $($_.Exception.Message)" -Level FATAL
}
finally {
if ($TranscriptStarted) {
try {
Stop-Transcript | Out-Null
Write-SCA_LogFile -Message "Transcript gestopt." -Level SUCCESS
}
catch {
Write-SCA_LogFile -Message "Stop-Transcript is mislukt: $($_.Exception.Message)" -Level ERROR
}
}
$IntuneLogPath = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs"
if ((Test-Path -Path $IntuneLogPath) -and (Test-Path -Path $TranscriptFile)) {
try {
Copy-Item -Path $TranscriptFile -Destination $IntuneLogPath -Force -ErrorAction Stop
Write-SCA_LogFile -Message "Transcript is gekopieerd naar de Intune-logmap." -Level SUCCESS
}
catch {
Write-SCA_LogFile -Message "Kopiëren naar de Intune-logmap is mislukt: $($_.Exception.Message)" -Level ERROR
}
}
}
#endregion Main
Waarom dit echt verschil maakt
Sinds ik dit gebruik:
- Geen “module not found” meer
- Geen handmatige installaties meer
- Geen gezoek naar versies
- Geen PSGallery drama (oké… minder drama 😅)
En vooral:
👉 Mijn scripts werken gewoon. Altijd.
Kleine extra bonus 💡
Ik heb hem zo gemaakt dat je ook kunt kiezen:
-ImportModules→ direct laden-ForceInstall→ alles opnieuw installeren-Beta→ leven op de rand van chaos
Conclusie
Soms zit de winst niet in fancy automation…
maar gewoon in het niet meer 20x hetzelfde stomme werk doen.
Dit script is daar een perfect voorbeeld van.
En eerlijk?
Ik had dit veel eerder moeten bouwen.