Skip to content

Van PowerShell tot Smart Home

Van PowerShell tot Smart Home

Primary Menu
  • Algemeen
  • Fotografie
  • Home Assistant
  • Microsoft
  • Azure
  • Powershell
Light/Dark Button
  • Home
  • Een PowerShell bootstrap voor modules, logging en minder irritatie
  • Powershell

Een PowerShell bootstrap voor modules, logging en minder irritatie

Vincent van Unen 2026-03-19 (Last updated: 2026-03-19) 17 minutes read
ChatGPT Image Mar 18, 2026, 11_44_05 AM

Er zijn twee zekerheden in het leven:

  1. Je drinkt koffie en die wordt koud.
  2. Op het moment dat je een PowerShell-script nodig hebt, ontbreekt er altijd nét weer een module.

En dat tweede begon me nogal te irriteren.

Elke keer als ik een machine opnieuw had ingericht, een frisse sessie startte of weer op een andere omgeving werkte, begon hetzelfde toneelstuk opnieuw:
module missen, module updaten, PSGallery corrigeren, repository fixen, transcript starten, logging regelen… en pas daarna kon ik eindelijk doen waarvoor ik PowerShell überhaupt had geopend.

Dus heb ik gedaan wat iedere licht gefrustreerde beheerder uiteindelijk doet:
ik heb er gewoon een script voor geschreven.

Niet omdat het leuk was.
Nou ja, een beetje wel.
Maar vooral omdat ik het zat was om steeds handmatig hetzelfde circus op te tuigen.

Waarom ik dit script heb gebouwd

Dit script is ontstaan uit pure praktische irritatie.

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.

Dus in plaats van elke keer dit te doen:

  • controleren of PSGallery nog leeft;
  • kijken of NuGet nog meewerkt;
  • modules handmatig installeren;
  • modules bijwerken;
  • transcript starten;
  • logging regelen;
  • hopen dat het deze keer wél netjes loopt;

… heb ik besloten dat PowerShell dat probleem zelf maar moest oplossen.

Deze bootstrap wordt nu geladen zodra ik een specifiek ander script uitvoer. Met andere woorden: vóórdat het echte werk begint, ruimt dit script eerst de rommel op. Een digitale versie van even de vaatwasser uitruimen voordat je gaat koken.

Wat dit script precies doet

De nieuwste versie van mijn script doet meer dan alleen “even modules installeren”.

Het script:

  • controleert logging en transcript;
  • maakt benodigde mappen aan zoals C:\Temp, C:\Tmp en C:\Log;
  • configureert PowerShell Gallery en PSResourceGet;
  • controleert of benodigde modules aanwezig zijn;
  • installeert of werkt modules automatisch bij;
  • ondersteunt zowel productie- als bètamodules;
  • kan modules direct importeren in de sessie;
  • logt netjes welke gebruiker, machine en IP-adressen zijn gebruikt;
  • kopieert het transcript zelfs naar de Intune-logmap als die aanwezig is.

Oftewel: het is niet zomaar een installatiescript, maar eerder een kleine PowerShell-conciërge die eerst alles netjes klaarzet voordat jij binnenkomt.

Wat er nieuw en beter is in deze versie

De update naar versie 3.1.1 is niet alleen cosmetisch. Hier zitten echt nuttige verbeteringen in.

1. Slimmere moduledefinities

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.

Dat klinkt heel professioneel, maar komt eigenlijk neer op:
ik kan PowerShell nu eindelijk fatsoenlijk vertellen wat ik wil, zonder dat het script daar emotioneel van instort.

2. Ondersteuning voor pinned modules

Sommige modules wil je niet automatisch naar de nieuwste versie trekken. Soms heb je gewoon een specifieke versie nodig omdat “nieuwer” niet altijd hetzelfde is als “beter”.

Daarom kan ik nu modules vastzetten op een vereiste versie, zoals:

CIS-M365-Benchmark op versie 5.2.0

Dat voorkomt verrassingen.
En met verrassingen bedoel ik natuurlijk: ellende.

3. Bootstrap-modules apart geregeld

De basis is nu opgesplitst in een eigen bootstrap-set met:

  • PackageManagement
  • PowerShellGet
  • Microsoft.PowerShell.PSResourceGet

Dat is slim, want als je afhankelijk bent van modulebeheer, moet je eerst zorgen dat je modulebeheer zélf op orde is. Een beetje alsof je eerst je gereedschapskist repareert voordat je de schuurdeur gaat fiksen.

4. Nettere fallback-logica

Als Find-Module, Install-Module of Update-Module niet meewerkt, probeert het script automatisch alternatieven via PSResourceGet.

Oftewel:
als de voordeur dicht zit, probeert het script gewoon de achterdeur.

Heel beschaafd overigens. Geen koevoet, gewoon nette PowerShell-logica.

Productie of bèta? Allebei kan

Het script ondersteunt twee modi:

Production

Voor de dagelijkse, serieuze beheerwerkzaamheden met modules zoals:

  • Az.Accounts
  • Microsoft.Graph
  • Microsoft.Entra
  • PnP.PowerShell
  • ExchangeOnlineManagement
  • MicrosoftTeams
  • CIS-M365-Benchmark

Beta

Voor de momenten waarop je denkt:
“Ik wil vooruitgang, spanning en een klein beetje risico.”

Dan kun je kiezen voor:

  • Az.Accounts
  • Microsoft.Graph.Beta
  • Microsoft.Entra.Beta

Waarom dit voor mij prettig werkt

Wat ik zelf fijn vind aan deze aanpak, is dat ik er daarna niet meer over hoef na te denken.

Ik start mijn andere script.
Deze bootstrap draait eerst.
Alle basisvoorwaarden worden gecontroleerd.
Modules worden bijgewerkt of geïnstalleerd.
Eventueel worden ze direct geladen.
En pas daarna begint het echte werk.

Dat betekent dus minder:

  • “Waarom doet dit script niets?”
  • “Oh wacht, module ontbreekt.”
  • “Waarom vertrouwt PSGallery zichzelf weer niet?”
  • “Waarom ben ik alweer handmatig dingen aan het fixen?”

En meer:

  • script starten;
  • koffie pakken;
  • doen waarvoor ik achter mijn laptop zat.

Het soort automatisering waar je echt blij van wordt

Niet alle automatisering hoeft groots, hip of AI-achtig te zijn.

Soms is de beste automatisering gewoon:
iets bouwen dat jouw eigen irritatie oplost.

Dat is precies wat dit script doet.

Geen glamour.
Geen marketingpraat.
Gewoon een robuust PowerShell-script dat voorkomt dat ik iedere keer opnieuw dezelfde administratieve strafwerkzaamheden moet uitvoeren.

En eerlijk?
Dat zijn vaak de fijnste scripts.
De scripts die niet indrukwekkend staan in een demo, maar die je op een drukke werkdag wél redden van nodeloos gevloek.

Script

param(
    [switch]$Silent,
    [switch]$Beta,
    [switch]$ImportModules,
    [switch]$ForceInstall
)

<#
.NOTES
============================================================================
 Created on:      2026-03-18
 Created by:      Vincent van Unen
  Filename:        SCA_PowerShell-Modules.ps1
============================================================================
.DESCRIPTION
 Initialisatiescript voor MSP-beheerwerkzaamheden.

 Het script controleert:
 - logging en transcript
 - mappenstructuur
 - PSGallery- en PSResourceGet-configuratie
 - aanwezigheid van vereiste PowerShell-modules

 Standaard worden productiemodules verwerkt.
 Gebruik -Beta om uitsluitend bètamodules te verwerken.
.SYNOPSIS
 Initialiseert PowerShell-modules met logging-, transcript- en repositoryfunctionaliteit.
.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       = '3.1.1'
$ScriptChangeDate    = '2026-03-19'
$ScriptChangeLog     = 'Bugfix voor hashtable-moduledefinities en ondersteuning voor pinned modules, inclusief CIS-M365-Benchmark'
$ScriptCurrentUser   = $env:UserName
$ScriptRunningDevice = $env:COMPUTERNAME
$CurrentDate         = Get-Date -Format 'yyyy-MM-dd'
$LogName             = 'Initialize-SCA-Modules'
$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 is mislukt: $($_.Exception.Message)" -ForegroundColor Red
    }
}
#endregion Logging functions

#region Helper functions
function Test-SCA_CommandExists {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue)
}

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 ConvertTo-SCA_ModuleObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Module
    )

    if ($Module -is [string]) {
        return [pscustomobject]@{
            Name            = $Module
            RequiredVersion = $null
        }
    }

    if ($Module -is [hashtable]) {
        if (-not $Module.ContainsKey('Name')) {
            throw 'Ongeldige moduledefinitie: sleutel "Name" ontbreekt.'
        }

        return [pscustomobject]@{
            Name            = [string]$Module['Name']
            RequiredVersion = if ($Module.ContainsKey('RequiredVersion')) { [string]$Module['RequiredVersion'] } else { $null }
        }
    }

    if ($Module.PSObject.Properties.Name -contains 'Name') {
        $RequiredVersion = $null

        if ($Module.PSObject.Properties.Name -contains 'RequiredVersion') {
            $RequiredVersion = [string]$Module.RequiredVersion
        }

        return [pscustomobject]@{
            Name            = [string]$Module.Name
            RequiredVersion = $RequiredVersion
        }
    }

    throw "Ongeldige moduledefinitie: $($Module | Out-String)"
}

function Initialize-SCA_PowerShellGet {
    [CmdletBinding()]
    param()

    if ($PSVersionTable.PSVersion.Major -ge 6) {
        Write-SCA_Status -Message 'PowerShell 6+ gedetecteerd. Oude PowerShellGet-initialisatie wordt beperkt toegepast.' -Level DEBUG
        Write-SCA_LogFile -Message 'PowerShell 6+ gedetecteerd. Oude PowerShellGet-initialisatie wordt beperkt toegepast.' -Level DEBUG
    }

    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ïnitialiseerd: $($_.Exception.Message)" -Level ERROR
        Write-SCA_LogFile -Message "NuGet-provider kon niet worden geïnitialiseerd: $($_.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 geïnstalleerd of bijgewerkt.' -Level SUCCESS
            Write-SCA_LogFile -Message 'PowerShellGet is geïnstalleerd of bijgewerkt.' -Level SUCCESS
        }
        else {
            Write-SCA_Status -Message "PowerShellGet is actueel. Versie: $($InstalledPowerShellGet.Version)" -Level SUCCESS
            Write-SCA_LogFile -Message "PowerShellGet is actueel. Versie: $($InstalledPowerShellGet.Version)" -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 package-management configureren...' -Level INFO
    Write-SCA_LogFile -Message 'PowerShell Gallery en package-management configureren...' -Level INFO

    try {
        Initialize-SCA_PowerShellGet

        try {
            $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
            }
        }
        catch {
            Write-SCA_Status -Message "PSGallery-configuratie via PowerShellGet is mislukt: $($_.Exception.Message)" -Level WARN
            Write-SCA_LogFile -Message "PSGallery-configuratie via PowerShellGet is mislukt: $($_.Exception.Message)" -Level WARN
        }

        if (Test-SCA_CommandExists -Name 'Register-PSResourceRepository') {
            try {
                $PSResourceRepo = Get-PSResourceRepository -Name 'PSGallery' -ErrorAction SilentlyContinue

                if (-not $PSResourceRepo) {
                    Register-PSResourceRepository -Name 'PSGallery' -Uri 'https://www.powershellgallery.com/api/v2' -Trusted -ErrorAction Stop
                    Write-SCA_Status -Message 'PSResourceGet-repository PSGallery is geregistreerd.' -Level SUCCESS
                    Write-SCA_LogFile -Message 'PSResourceGet-repository PSGallery is geregistreerd.' -Level SUCCESS
                }
                else {
                    Write-SCA_Status -Message 'PSResourceGet-repository PSGallery is beschikbaar.' -Level SUCCESS
                    Write-SCA_LogFile -Message 'PSResourceGet-repository PSGallery is beschikbaar.' -Level SUCCESS
                }
            }
            catch {
                Write-SCA_Status -Message "PSResourceGet-repositoryconfiguratie is mislukt: $($_.Exception.Message)" -Level WARN
                Write-SCA_LogFile -Message "PSResourceGet-repositoryconfiguratie is mislukt: $($_.Exception.Message)" -Level WARN
            }
        }
    }
    catch {
        Write-SCA_Status -Message "Repositoryconfiguratie is mislukt: $($_.Exception.Message)" -Level ERROR
        Write-SCA_LogFile -Message "Repositoryconfiguratie is mislukt: $($_.Exception.Message)" -Level ERROR
        throw
    }
}

function Get-SCA_InstalledModuleVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    try {
        if (Test-SCA_CommandExists -Name 'Get-InstalledPSResource') {
            $InstalledPSResource = Get-InstalledPSResource -Name $Name -ErrorAction SilentlyContinue |
                Sort-Object Version -Descending |
                Select-Object -First 1

            if ($InstalledPSResource) {
                return [pscustomobject]@{
                    Name    = $InstalledPSResource.Name
                    Version = $InstalledPSResource.Version
                    Source  = 'PSResourceGet'
                }
            }
        }

        $InstalledModule = Get-InstalledModule -Name $Name -ErrorAction SilentlyContinue |
            Sort-Object Version -Descending |
            Select-Object -First 1

        if ($InstalledModule) {
            return [pscustomobject]@{
                Name    = $InstalledModule.Name
                Version = $InstalledModule.Version
                Source  = 'PowerShellGet'
            }
        }

        return $null
    }
    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 (Test-SCA_CommandExists -Name 'Find-PSResource') {
                $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,

        [string]$RequiredVersion
    )

    try {
        $Params = @{
            Name         = $Name
            Repository   = 'PSGallery'
            Scope        = 'CurrentUser'
            Force        = $true
            AllowClobber = $true
            ErrorAction  = 'Stop'
        }

        if ($RequiredVersion) {
            $Params.RequiredVersion = $RequiredVersion
        }

        Install-Module @Params
        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 (Test-SCA_CommandExists -Name 'Install-PSResource') {
                $PSResourceParams = @{
                    Name            = $Name
                    Repository      = 'PSGallery'
                    Scope           = 'CurrentUser'
                    TrustRepository = $true
                    Force           = $true
                    ErrorAction     = 'Stop'
                }

                if ($RequiredVersion) {
                    $PSResourceParams.Version = $RequiredVersion
                }

                Install-PSResource @PSResourceParams
                return $true
            }
        }
        catch {
            Write-SCA_Status -Message "Install-PSResource is mislukt voor $Nam : $($_.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. Nieuwe installatie wordt geprobeerd." -Level WARN
        Write-SCA_LogFile -Message "Update-Module is mislukt voor $Name. Nieuwe installatie wordt geprobeerd." -Level WARN
        return (Install-SCA_Module -Name $Name)
    }
}

function InstallOrUpdate-SCA_Module {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [string]$RequiredVersion,

        [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

    if ($RequiredVersion) {
        if (-not $Installed) {
            Write-SCA_Status -Message "$Name is niet geïnstalleerd. Installatie van vaste versie $RequiredVersion wordt gestart." -Level WARN
            Write-SCA_LogFile -Message "$Name is niet geïnstalleerd. Installatie van vaste versie $RequiredVersion wordt gestart." -Level WARN

            if (Install-SCA_Module -Name $Name -RequiredVersion $RequiredVersion) {
                Write-SCA_Status -Message "$Name is succesvol geïnstalleerd op versie $RequiredVersion." -Level SUCCESS
                Write-SCA_LogFile -Message "$Name is succesvol geïnstalleerd op versie $RequiredVersion." -Level SUCCESS
            }
            else {
                Write-SCA_Status -Message "Installatie is mislukt voor $Name versie $RequiredVersion." -Level ERROR
                Write-SCA_LogFile -Message "Installatie is mislukt voor $Name versie $RequiredVersion." -Level ERROR
            }

            return
        }

        Write-SCA_Status -Message "$Name lokaal: $($Installed.Version) | vereist: $RequiredVersion" -Level INFO
        Write-SCA_LogFile -Message "$Name lokaal: $($Installed.Version) | vereist: $RequiredVersion" -Level INFO

        if ($ForceInstall -or ([version]$Installed.Version -ne [version]$RequiredVersion)) {
            Write-SCA_Status -Message "$Name wordt geïnstalleerd of gecorrigeerd naar versie $RequiredVersion." -Level WARN
            Write-SCA_LogFile -Message "$Name wordt geïnstalleerd of gecorrigeerd naar versie $RequiredVersion." -Level WARN

            if (Install-SCA_Module -Name $Name -RequiredVersion $RequiredVersion) {
                Write-SCA_Status -Message "$Name is succesvol verwerkt op versie $RequiredVersion." -Level SUCCESS
                Write-SCA_LogFile -Message "$Name is succesvol verwerkt op versie $RequiredVersion." -Level SUCCESS
            }
            else {
                Write-SCA_Status -Message "Installatie is mislukt voor $Name versie $RequiredVersion." -Level ERROR
                Write-SCA_LogFile -Message "Installatie is mislukt voor $Name versie $RequiredVersion." -Level ERROR
            }
        }
        else {
            Write-SCA_Status -Message "$Name staat al op de vereiste versie $RequiredVersion." -Level SUCCESS
            Write-SCA_LogFile -Message "$Name staat al op de vereiste versie $RequiredVersion." -Level SUCCESS
        }

        return
    }

    $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)]
        [object[]]$Modules
    )

    foreach ($ModuleDefinition in $Modules) {
        $Module = ConvertTo-SCA_ModuleObject -Module $ModuleDefinition

        try {
            Import-Module $Module.Name -Force -ErrorAction Stop
            Write-SCA_Status -Message "$($Module.Name) is geladen in de sessie." -Level SUCCESS
            Write-SCA_LogFile -Message "$($Module.Name) is geladen in de sessie." -Level SUCCESS
        }
        catch {
            Write-SCA_Status -Message "Importeren is mislukt voor $($Module.Name): $($_.Exception.Message)" -Level ERROR
            Write-SCA_LogFile -Message "Importeren is mislukt voor $($Module.Name): $($_.Exception.Message)" -Level ERROR
        }
    }
}
#endregion Helper functions

#region Module configuration
$ModuleSets = @{
    Bootstrap = @(
        @{ Name = 'PackageManagement' },
        @{ Name = 'PowerShellGet' },
        @{ Name = 'Microsoft.PowerShell.PSResourceGet' }
    )

    Production = @(
        @{ Name = 'Az.Accounts' },
        @{ Name = 'Microsoft.Graph' },
        @{ Name = 'Microsoft.Entra' },
        @{ Name = 'PnP.PowerShell' },
        @{ Name = 'Microsoft.Online.SharePoint.PowerShell' },
        @{ Name = 'ExchangeOnlineManagement' },
        @{ Name = 'MicrosoftTeams' },
        @{ Name = 'CIS-M365-Benchmark'; RequiredVersion = '5.2.0' }
    )

    Beta = @(
        @{ Name = 'Az.Accounts' },
        @{ Name = 'Microsoft.Graph.Beta' },
        @{ Name = 'Microsoft.Entra.Beta' }
    )
}

$SelectedModules = if ($Beta) {
    $ModuleSets.Beta
}
else {
    $ModuleSets.Production
}

$AllModules = @($ModuleSets.Bootstrap + $SelectedModules)
#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 "Bootstrap modules = $((($ModuleSets.Bootstrap | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))"
Write-SCA_LogFile -Message "Selected modules = $((($SelectedModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))"
Write-SCA_LogFile -Message "All modules = $((($AllModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))"

Write-SCA_Status -Message "Scriptmodus: $ScriptMode" -Level INFO
Write-SCA_Status -Message "Bootstrapmodules: $((($ModuleSets.Bootstrap | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -join ', '))" -Level INFO
Write-SCA_Status -Message "Geselecteerde modules: $((($SelectedModules | ForEach-Object { (ConvertTo-SCA_ModuleObject -Module $_).Name }) -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 ($ModuleDefinition in $AllModules) {
        $Module = ConvertTo-SCA_ModuleObject -Module $ModuleDefinition

        InstallOrUpdate-SCA_Module `
            -Name $Module.Name `
            -RequiredVersion $Module.RequiredVersion `
            -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 "Het script is beëindigd met een fout: $($_.Exception.Message)" -Level ERROR
    Write-SCA_LogFile -Message "Het 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

Conclusie

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.

Dus heb ik het proces geautomatiseerd.

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.

Kort gezegd:
ik heb een terugkerend probleem vervangen door een script.

Zoals het hoort.

About the Author

Vincent van Unen

Administrator

Visit Website View All Posts

Post navigation

Previous: 🔧 Je netwerk echt meten met PowerShell

Related Stories

ChatGPT Image Mar 17, 2026, 01_17_55 PM
  • Powershell

🔧 Je netwerk echt meten met PowerShell

Vincent van Unen 2026-03-17
ChatGPT Image Mar 13, 2026, 05_39_33 AM
  • Azure
  • Powershell

🔎 Even snel de Tenant ID van een domein ophalen met PowerShell

Vincent van Unen 2026-03-13
ChatGPT Image Mar 13, 2026, 10_06_00 AM
  • Azure
  • Powershell

Powershell : Tijdelijke toegang zonder gedoe: Temporary Access Pass automatiseren

Vincent van Unen 2026-03-13

Recent Posts

  • Een PowerShell bootstrap voor modules, logging en minder irritatie
  • 🔧 Je netwerk echt meten met PowerShell
  • Slimme ESPHome multisensor: UN-SENSOR-12
  • 🔎 Even snel de Tenant ID van een domein ophalen met PowerShell
  • Powershell : Tijdelijke toegang zonder gedoe: Temporary Access Pass automatiseren

Recent Comments

No comments to show.

You May Have Missed

ChatGPT Image Mar 18, 2026, 11_44_05 AM
  • Powershell

Een PowerShell bootstrap voor modules, logging en minder irritatie

Vincent van Unen 2026-03-19
ChatGPT Image Mar 17, 2026, 01_17_55 PM
  • Powershell

🔧 Je netwerk echt meten met PowerShell

Vincent van Unen 2026-03-17
ChatGPT Image Mar 17, 2026, 09_52_02 AM
  • ESPHOME
  • Home Assistant

Slimme ESPHome multisensor: UN-SENSOR-12

Vincent van Unen 2026-03-17
ChatGPT Image Mar 13, 2026, 05_39_33 AM
  • Azure
  • Powershell

🔎 Even snel de Tenant ID van een domein ophalen met PowerShell

Vincent van Unen 2026-03-13
  • Algemeen
  • Fotografie
  • Home Assistant
  • Microsoft
  • Azure
  • Powershell
  • Algemeen
  • Fotografie
  • Home Assistant
  • Microsoft
  • Azure
  • Powershell
Copyright © 2026 All rights reserved. | ReviewNews by AF themes.