Microsoft 365 Licentie-optimalisatie Met FinOps-besturing

💼 Management Samenvatting

Licentiekosten vormen gemiddeld dertig procent van het Microsoft 365-budget bij Nederlandse overheidsorganisaties. Zonder scherp inzicht vervagen grenzen tussen noodzakelijke beveiligingscapaciteit en dure, ongebruikte add-ons. Licentie-optimalisatie is daarom geen boekhoudkundige exercitie maar een veiligheidsmaatregel die directe impact heeft op continuïteit en NIS2-verplichtingen.

Aanbeveling
OPTIMALISEER
Risico zonder
Medium
Risk Score
7/10
Implementatie
115u (tech: 60u)
Van toepassing op:
Microsoft 365
Microsoft Teams
Microsoft Defender
Microsoft Intune
Entra ID

Een versnipperde licentiehuishouding leidt tot dubbele accounts, vergeten proefpakketten en ongestructureerde delegatie van beheerrechten aan leveranciers. Elke ongebruikte E5-suite betekent niet alleen verspild budget maar ook een uitbreidingspunt voor privileges en auditing. FinOps-principes schrijven voor dat kosten, verbruik en risico's elkaar continu spiegelen. Met de komst van Copilot en aanvullende beveiligingsbundels loopt het aantal SKU's explosief op, waardoor beleid zonder centrale regie binnen maanden verouderd is.

PowerShell Modules Vereist
Primary API: Microsoft Graph Reports API, Microsoft Graph Licenses, Azure Commerce
Connection: Connect-MgGraph (Reports.Read.All, Directory.Read.All, AuditLog.Read.All) en geauthenticeerde Invoke-MgGraphRequest-calls naar /reports en /subscribedSkus endpoints
Required Modules: Microsoft.Graph, Microsoft.Graph.Reports

Implementatie

Dit artikel beschrijft hoe u licentie-optimalisatie organiseert volgens de Nederlandse Baseline voor Veilige Cloud: van risicobeoordeling, dataverzameling en scenario-analyse tot governance, rapportage en geautomatiseerde opvolging. U leert hoe het script license-optimization.ps1 Microsoft Graph-data combineert met FinOps-kpi's, hoe u debug-tests lokaal uitvoert binnen vijftien seconden en hoe de resultaten aansluiten op begrotingscycli, audittrail en besluitvorming.

Urgentie, risico's en FinOps-kaders voor licenties

Nederlandse overheden rapporteren jaarlijks aan ministeries, toezichthouders en gemeenteraden over digitale bestedingen. Licenties zijn daarin een van de grootste posten en tegelijk het moeilijkst uit te leggen. Een E5-beveiligingssuite combineert identity, compliance en analytics, maar wanneer slechts een klein deel van de features wordt gebruikt, ontstaat een governance-probleem: de organisatie betaalt voor functionaliteit die nooit wordt geactiveerd terwijl risicovolle workloads ongedekt blijven. FinOps vraagt daarom om zicht op waarde per euro, niet alleen om korting op volumedeals. Een licentie die geen meetbare risicoreductie levert, hoort niet in een zero-trust architectuur. Door licentie-optimalisatie te positioneren als beveiligingsmaatregel wordt duidelijk dat dit geen bezuiniging is, maar een herallocatie naar de controles die nodig zijn voor compliance en incidentrespons.

De Baseline Informatiebeveiliging Overheid (BIO) en NIS2 eisen aantoonbare doelmatigheid. Een toezichthouder kijkt niet alleen naar het bestaan van maatregelen, maar ook naar de proportionele inzet van middelen. Een organisatie die wel dure insider-risk licenties bezit maar geen operationeel team heeft om alerts te verwerken, handelt in strijd met het principle of accountability. Bovendien leidt licentieproliferatie tot privacyrisico's: hoe meer diensten zijn ingeschakeld, hoe complexer dataminimalisatie en logging worden. Door licentie-optimalisatie als onderdeel van het interne controlesysteem te beschouwen, kunnen CISO en CFO gezamenlijk aantonen dat elke licentie gekoppeld is aan een risicodoelstelling, proces of wettelijke verplichting. Zo ontstaat een auditspoor waarin per SKU staat beschreven waarom deze nodig is, welke dataset hij beschermt en welk bestuursbesluit eraan ten grondslag ligt.

Een tweede risicofactor is de dynamiek van personeelsmutaties, ketensamenwerkingen en tijdelijke programma's. Projectorganisaties huren snel honderden extra accounts in voor leveranciers, trainees of verkiezingswaarnemers. Worden deze licentieclaims niet binnen dagen tegengezet door offboarding, dan lopen license pools vol en rest slechts de aankoop van aanvullende bundels. Dat vergroot ook het aanvalsoppervlak: slapende accounts met E5-licenties beschikken vaak automatisch over admin portals, auditlogs en andere gevoelige modules. Door licentie-optimalisatie te integreren in onboarding-processen inclusief automatische verlopen, blijft het aantal actieve rechten beheersbaar en dalen zowel kosten als risico's. Het FinOps-gedachtegoed voorziet daarom in policy guardrails waarin elke licentietoevoeging een vervaldatum, eigenaar en justificatie meekrijgt.

Tot slot is er de afhankelijkheid van versnipperde data. Licentie-informatie woont in Commerce, Microsoft 365 Admin center, factuurportalen en losse Excel-overzichten per afdeling. Zonder uniforme dataset ontstaan interpretatieverschillen: finance rekent in contractterminen, IT in toegewezen seats en security in feitelijk gebruik. Deze inconsistenties zijn precies de reden waarom FinOps een productteam introduceert dat data-engineering, security en controlling combineert. De Nederlandse Baseline voor Veilige Cloud adviseert daarom één bron van waarheid op basis van Microsoft Graph-data. Door elke nacht dezelfde exports te draaien en de resultaten automatisch te vergelijken met CMDB en HR-gegevens, verdwijnt discussie en ontstaat ruimte voor strategische optimalisatie. Het script dat bij dit artikel hoort, voorziet in deze uniforme dataset en vormt het technische fundament onder de besluitvorming.

Datagedreven analyse en scenario's voor licentie-optimalisatie

Een volwassen aanpak begint met een inventarisatie van alle Subscribed SKUs, toewijzingen per workload en het effectieve gebruik van functionaliteiten zoals Defender for Office, Purview DLP en Copilot. Microsoft Graph levert hiervoor drie essentiële datasets: SubscribedSkus voor contractuele rechten, User objecten inclusief sign-in activiteit voor toegewezen seats, en rapportages uit het /reports-endpoint voor feitelijk gebruik. Door deze bronnen te combineren ontstaat inzicht in bezettingsgraad per SKU, inactieve accounts per licentie en trends in adoptie. Dit artikel hanteert een standaardmodel waarin iedere licentie wordt beoordeeld op drie assen: kriticiteit voor compliance, werkelijke benutting en kostprijs. Alleen licenties die op alle assen voldoende scoren blijven ongemoeid; de rest gaat naar de optimalisatie backlog.

Scenario-analyse is de volgende stap. Bestuurders willen weten wat er gebeurt bij reorganisaties, verkiezingen of de invoering van Copilot. Daarom definieert u minimaal drie scenario's: "baseload" (huidige inzet), "inspelen op NIS2" (extra beveiligingsmaatregelen) en "flexpool" (tijdelijke opschaling). Voor elk scenario berekent u de licentiedruk, benodigde budgetten, fte-impact voor beheer en het effect op KPI's zoals Secure Score of MTTR. Door de scenario's te koppelen aan een besluitkalender kunnen bestuurders in de begrotingscyclus kiezen tussen verschillende optimalisatiepaden. Het bijbehorende JSON-bestand documenteert deze scenario's zodat auditors precies kunnen volgen welke aannames zijn gehanteerd en hoe deze terugkomen in investeringsbesluiten.

Data-kwaliteitscontrole is cruciaal. Iedere run van het licentie-rapport moet aantonen hoeveel records zijn verwerkt, welke API-calls zijn gedaan, welke filters zijn toegepast en of er afwijkingen zijn geconstateerd. Denk aan gebruikers zonder afdeling, service accounts met premium licenties of leveranciers waarvoor dubbele tenants zijn aangemaakt. Het script genereert daarom validatieregels die elke nacht meedraaien en die de uitkomst markeren als "niet betrouwbaar" wanneer datasets ontbreken. Alleen zo kan een auditor vertrouwen op de cijfers en kan de organisatie aan de Archiefwet-verplichting voldoen door ruwe data minimaal zeven jaar te bewaren.

Het analyseresultaat mondt uit in concrete FinOps- en security-metrics: percentage ongebruikte seats, aantal inactieve accounts ouder dan dertig dagen per SKU, verhouding E3 versus E5 op privileged accounts en besparingspotentieel door het consolideren van add-ons naar suites. Deze metrics worden gekoppeld aan beslisregels, bijvoorbeeld "als meer dan vijf procent van de E5-accounts langer dan 30 dagen inactief is, reduceer licenties" of "wanneer Defender for Office alerts niet worden opgevolgd binnen het SOC, heroverweeg add-on licenties". Door metric en beslisregel samen op te nemen in de JSON ontstaat een volledige audit trail.

Automatisering, scriptondersteuning en lokale debugruns

Gebruik PowerShell-script license-optimization.ps1 (functie Invoke-LicenseOptimizationAssessment) – Verbindt met Microsoft Graph, inventariseert Subscribed SKUs, koppelt gebruikersactiviteit en genereert een FinOps-dashboard inclusief risicoscore, besparingspotentieel en KPI's zoals ongebruikte E5-licenties..

Gebruik PowerShell-script license-optimization.ps1 (functie Invoke-LicenseOptimizationRemediation) – Produceert een remediatieplan per SKU met concrete acties (conversie naar E3, offboarding, herverdeling naar SOC-teams) en exporteert optioneel een CSV/JSON-bestand voor ITSM-workflows..

Het PowerShell-script vormt de harde ruggengraat van de optimalisatiecyclus. Het script gebruikt Connect-MgGraph met de scopes Reports.Read.All en Directory.Read.All, leest SubscribedSkus, haalt gebruikers inclusief AssignedLicenses en SignInActivity op en combineert deze data in een rapportageobject. Elke run bevat een tijdstempel, de gebruikte scopes en een hash van de ruwe dataset zodat wijzigingen achteraf zijn te reconstrueren. Het script detecteert licenties die langer dan een ingestelde drempel ongebruikt zijn, vergelijkt toegewezen en beschikbare seats en markeert onderbenutte add-ons zoals Defender for Identity wanneer er geen actieve alerts worden gelogd. Omdat het script in dezelfde repository staat als deze JSON, blijft documentatie en tooling gekoppeld en wordt hergebruik eenvoudig.

DebugMode maakt lokale testen mogelijk binnen vijftien seconden, conform de projectafspraken. In deze modus worden synthetische datasets geladen die de meest voorkomende scenario's vertegenwoordigen: een te ruim ingestelde E5-bundel, een overvolle Business Premium pool en een project met tijdelijke licenties. Hierdoor kan een ontwikkelaar nieuwe regels toevoegen, errorhandling testen en exportformaten controleren zonder productiegegevens te benaderen. Zodra DebugMode uitstaat, draait het script alle Graph-calls met exponential back-off en bewaakt het de totale uitvoeringstijd. Overschrijdt een run vijftien seconden, dan logt het script een waarschuwing en adviseert het om filters te verfijnen of de dataset te partitioneren over meerdere automation jobs.

De exportfunctie ondersteunt JSON en CSV zodat de uitkomsten zowel naar Power BI als naar juridische dossiers kunnen worden geschreven. Iedere export bevat een uitgesplitste lijst van permanente toewijzingen, inactieve accounts per SKU, scenarioresultaten en een overzicht van aanbevolen acties met prioriteit en verwachte euro-besparing. Voor continue verbetering kan het script worden gekoppeld aan Azure Automation of GitHub Actions waar een dagelijkse run een statusbadge publiceert in het licentieteam-kanaal. Dankzij de duidelijke functieopbouw zijn uitbreidingen eenvoudig, bijvoorbeeld het opnemen van Copilot- of Viva-licenties of het koppelen van FinOps-tagging vanuit Azure Cost Management.

Storingsbestendigheid is ingebouwd via retries, duidelijke foutmeldingen en exitcodes die CI/CD-pijplijnen begrijpen. Een exitcode 0 betekent dat licenties binnen de drempel vallen, 1 staat voor detecties die managementaandacht vergen en 2 signaleert dat data onvolledig is. Deze codes worden in het security board besproken en vormen input voor sprintplanning. Het script schrijft tevens een compacte changelog naar het logboek in de deployment-map zodat auditors zien wanneer logica is aangepast. Daarmee voldoet de organisatie aan de verplichting om toolingwijzigingen te documenteren en aan lokale debug-eisen voordat iets naar productie gaat.

Governance, besluitvorming en rapportageketen

Effectieve licentie-optimalisatie vereist een governance-structuur waarin CISO, CIO, controller en inkoop samenwerken. Elke maand behandelt het security board een licentierapportage die de kerncijfers toont en afwijkingen labelt met risicokleur. Besluiten over downsizing of herallocatie worden vastgelegd in hetzelfde systeem als technische wijzigingen, inclusief verwijzing naar de rapportage die het besluit onderbouwt. Hierdoor kan een auditor een directe lijn trekken van Graph-data naar bestuursbesluit, iets dat nadrukkelijk wordt gevraagd in de Rekenkamer en NIS2-context.

Rapportages zijn meerlagig: een executive dashboard met KPI's als "bespaarde licentiekosten", "percentage inactieve E5-seats" en "Copilot-adoptiegraad"; een operationeel rapport met detaildata per afdeling; en een auditorset met ruwe exports, hashwaarden en scriptlogs. Door alle lagen vanuit hetzelfde script te voeden voorkomt u verkeerde versies en discussie over definities. Bovendien kunnen controllers de data direct koppelen aan de planning-en-controlcyclus zodat begrotingen sneller kunnen worden aangepast naarmate het gebruik verandert.

De governanceketen strekt zich ook uit tot leveranciers. Contracten met resellers bevatten voortaan de verplichting om licentiedata machineleesbaar aan te leveren volgens het formaat van dit artikel. Daarmee vervallen onafhankelijke spreadsheets en ontstaat ruimte voor near-real-time reconciliatie tussen facturen en Graph-data. In uitbestedingssituaties (bijvoorbeeld shared services of regionale samenwerkingen) wordt de rapportage centraal gedeeld zodat iedere deelnemer dezelfde KPI's en verbeteracties ziet. Dit voorkomt dat optimalisaties in de ene tenant teniet worden gedaan door groeiplannen in een andere.

Tot slot borgt u continuïteit via beleid en training. Elke beheerder die licenties mag aanpassen, volgt een jaarlijkse training waarin het FinOps-framework, het script en de rapportagecyclus worden behandeld. Offboardingprocedures bevatten een verplichte licentiecontrole en auditors toetsen steekproefsgewijs of beslisregels goed worden toegepast. Door lessons learned en savings zichtbaar te maken in interne communicatie vergroot u het draagvlak. Wanneer bestuurders zien dat herallocatie van E5-licenties budget vrijmaakt voor bijvoorbeeld zero-trust netwerksegmentatie, wordt licentie-optimalisatie een strategisch instrument in plaats van een incidentele bezuiniging.

Compliance & Frameworks

Automation

Gebruik het onderstaande PowerShell script om deze security control te monitoren en te implementeren. Het script bevat functies voor zowel monitoring (-Monitoring) als remediation (-Remediation).

PowerShell
<# .SYNOPSIS Microsoft 365 licentie-optimalisatie voor de Nederlandse Baseline voor Veilige Cloud. .DESCRIPTION Verzamelt Microsoft Graph-data over Subscribed SKUs en toegewezen gebruikers, berekent FinOps- en beveiligingsindicatoren (zoals ongebruikte seats, inactieve accounts en besparingspotentieel) en levert een exporteerbaar rapport. Ondersteunt een remediatieplan dat acties groepeert per SKU. DebugMode laadt synthetische data zodat lokale tests binnen 15 seconden kunnen plaatsvinden zonder productieverbinding. .NOTES Project : Nederlandse Baseline voor Veilige Cloud Author : Nederlandse Baseline voor Veilige Cloud Team Version : 1.0.0 Requires: Microsoft.Graph, Microsoft.Graph.Reports .EXAMPLE .\license-optimization.ps1 -Assessment Voert de hele analyse uit, toont bevindingen en geeft exitcodes (0 = compliant, 1 = acties nodig). .EXAMPLE .\license-optimization.ps1 -Assessment -OutputPath .\reports\licenses.json Draait de analyse en exporteert het volledige rapport naar JSON. .EXAMPLE .\license-optimization.ps1 -RemediationPlan -DebugMode Laadt de synthetische dataset, genereert een actieplan en toont geen productiegegevens. #> #Requires -Version 5.1 #Requires -Modules Microsoft.Graph, Microsoft.Graph.Reports [CmdletBinding(DefaultParameterSetName = "Assessment")] param( [Parameter(Mandatory = $true, ParameterSetName = "Assessment")] [switch]$Assessment, [Parameter(Mandatory = $true, ParameterSetName = "Remediation")] [switch]$RemediationPlan, [Parameter(Mandatory = $false)] [switch]$DebugMode, [Parameter(Mandatory = $false)] [string]$OutputPath, [Parameter(Mandatory = $false)] [ValidateSet("Json", "Csv")] [string]$OutputFormat = "Json", [Parameter(Mandatory = $false)] [ValidateRange(7, 120)] [int]$UnusedThresholdDays = 30 ) $ErrorActionPreference = 'Stop' function Write-Section { param( [Parameter(Mandatory = $true)][string]$Message, [Parameter()][ConsoleColor]$Color = [ConsoleColor]::Cyan ) Write-Host "`n==== $Message ====" -ForegroundColor $Color } function Connect-LicenseGraph { [CmdletBinding()] param() if ($DebugMode) { return } try { $context = Get-MgContext -ErrorAction SilentlyContinue if (-not $context) { Write-Host "[INFO] Verbinding maken met Microsoft Graph..." -ForegroundColor Gray Connect-MgGraph -Scopes @( "Reports.Read.All", "Directory.Read.All", "AuditLog.Read.All" ) -NoWelcome -ErrorAction Stop } } catch { throw "Kon geen Graph-verbinding opzetten: $($_.Exception.Message)" } } function Get-DebugDataset { [CmdletBinding()] param() $now = Get-Date $skus = @( [pscustomobject]@{ SkuId = [guid]::NewGuid() SkuPartNumber = "SPE_E5" DisplayName = "Microsoft 365 E5" ConsumedUnits = 480 PrepaidUnits = [pscustomobject]@{ Enabled = 500 } ServicePlans = @() }, [pscustomobject]@{ SkuId = [guid]::NewGuid() SkuPartNumber = "ENTERPRISEPREMIUM" DisplayName = "Microsoft 365 E3" ConsumedUnits = 950 PrepaidUnits = [pscustomobject]@{ Enabled = 1000 } ServicePlans = @() } ) $users = @( [pscustomobject]@{ Id = [guid]::NewGuid().Guid DisplayName = "Projectleider Verkiezingen" UserPrincipalName = "projectleider@voorbeeld.nl" Department = "Verkiezingen" AssignedLicenses = @(@{ SkuId = $skus[0].SkuId }) SignInActivity = @{ LastSignInDateTime = $now.AddDays(-65).ToString("o") } }, [pscustomobject]@{ Id = [guid]::NewGuid().Guid DisplayName = "SOC-analist" UserPrincipalName = "socanalist@voorbeeld.nl" Department = "SOC" AssignedLicenses = @(@{ SkuId = $skus[0].SkuId }) SignInActivity = @{ LastSignInDateTime = $now.AddDays(-2).ToString("o") } }, [pscustomobject]@{ Id = [guid]::NewGuid().Guid DisplayName = "Tijdelijke leverancier" UserPrincipalName = "leverancier@voorbeeld.nl" Department = "Project Digitale Identiteit" AssignedLicenses = @(@{ SkuId = $skus[1].SkuId }) SignInActivity = @{ LastSignInDateTime = $null } } ) return [pscustomobject]@{ Skus = $skus Users = $users Usage = @() } } function Get-LicenseInventory { [CmdletBinding()] param() if ($DebugMode) { return Get-DebugDataset } Connect-LicenseGraph Write-Host "[INFO] Licentiegegevens ophalen..." -ForegroundColor Gray $skus = Get-MgSubscribedSku -All -ErrorAction Stop Write-Host "[INFO] Gebruikers en toewijzingen verzamelen..." -ForegroundColor Gray $users = Get-MgUser -All -ConsistencyLevel eventual -CountVariable _ -Property ` "DisplayName", "UserPrincipalName", "Department", "AssignedLicenses", "SignInActivity" $usage = @() try { $uri = "https://graph.microsoft.com/v1.0/reports/getOffice365ActiveUserCounts(period='D30')" $response = Invoke-MgGraphRequest -Method GET -Uri $uri -OutputType HttpResponseMessage if ($response.StatusCode -eq 200) { $csv = $response.Content.ReadAsStringAsync().Result if ($csv) { $usage = ($csv -split "`r?`n" | Where-Object { $_ -and ($_ -notmatch "^#") }) | ConvertFrom-Csv } } } catch { Write-Warning "Kon gebruiksrapport niet ophalen: $($_.Exception.Message)" } return [pscustomobject]@{ Skus = $skus Users = $users Usage = $usage } } function Measure-LicenseUtilization { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Inventory ) $now = Get-Date $licenseReport = @() $inactiveAssignments = @() $globalFindings = @() foreach ($sku in $Inventory.Skus) { $assignedUsers = @() foreach ($user in $Inventory.Users) { if (-not $user.AssignedLicenses) { continue } $hasSku = $false foreach ($license in $user.AssignedLicenses) { if ($license.SkuId -eq $sku.SkuId) { $hasSku = $true break } } if ($hasSku) { $assignedUsers += $user } } $inactiveForSku = @() foreach ($user in $assignedUsers) { $lastSignIn = $null if ($user.SignInActivity -and $user.SignInActivity.LastSignInDateTime) { $lastSignIn = [datetime]$user.SignInActivity.LastSignInDateTime } $daysInactive = if ($lastSignIn) { [math]::Round((($now - $lastSignIn).TotalDays), 1) } else { [double]::PositiveInfinity } if ($daysInactive -ge $UnusedThresholdDays) { $inactiveAssignments += [pscustomobject]@{ SkuPartNumber = $sku.SkuPartNumber User = $user.UserPrincipalName Department = $user.Department DaysInactive = $daysInactive } $inactiveForSku += $user } } $enabledSeats = [int]($sku.PrepaidUnits.Enabled) $consumed = [int]$sku.ConsumedUnits $unusedPercent = if ($enabledSeats -gt 0) { [math]::Round((($enabledSeats - $consumed) / $enabledSeats) * 100, 2) } else { 0 } if ($inactiveForSku.Count -gt 0 -or $unusedPercent -gt 5) { $globalFindings += [pscustomobject]@{ SkuPartNumber = $sku.SkuPartNumber AssignedUsers = $assignedUsers.Count InactiveUsers = $inactiveForSku.Count UnusedPercentage = $unusedPercent Finding = "Onderbenutte licenties of inactieve accounts gedetecteerd" } } $licenseReport += [pscustomobject]@{ SkuPartNumber = $sku.SkuPartNumber DisplayName = $sku.DisplayName EnabledSeats = $enabledSeats ConsumedSeats = $consumed AssignedUsers = $assignedUsers.Count AvailableSeats = $enabledSeats - $consumed InactiveUsers = $inactiveForSku.Count UnusedPercentage = $unusedPercent } } return [pscustomobject]@{ Timestamp = $now ThresholdDays = $UnusedThresholdDays Source = if ($DebugMode) { "DebugDataset" } else { "Microsoft Graph" } Skus = $licenseReport InactiveAssignments = $inactiveAssignments UsageSummary = $Inventory.Usage Findings = $globalFindings } } function Write-LicenseSummary { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Report ) Write-Section -Message "Licentie-analyse" -Color Yellow Write-Host ("Bron: {0}" -f $Report.Source) -ForegroundColor Gray Write-Host ("Drempel inactief (dagen): {0}" -f $Report.ThresholdDays) -ForegroundColor Gray foreach ($sku in $Report.Skus | Sort-Object { $_.UnusedPercentage } -Descending) { Write-Host ("- {0} ({1}): {2}/{3} seats gebruikt, {4} inactieve gebruikers, {5}% ongebruikt" -f ` $sku.DisplayName, $sku.SkuPartNumber, $sku.ConsumedSeats, $sku.EnabledSeats, ` $sku.InactiveUsers, $sku.UnusedPercentage) -ForegroundColor ` ($(if ($sku.UnusedPercentage -gt 10) { "Yellow" } else { "Green" })) } if ($Report.Findings.Count -gt 0) { Write-Host "`n[ALERT] Bevindingen die bestuur aandacht vereisen:" -ForegroundColor Yellow foreach ($finding in $Report.Findings) { Write-Host (" * {0}: {1} inactief, {2}% ongebruikt" -f ` $finding.SkuPartNumber, $finding.InactiveUsers, $finding.UnusedPercentage) ` -ForegroundColor Gray } } else { Write-Host "`n[OK] Geen kritieke bevindingen gedetecteerd." -ForegroundColor Green } } function Export-LicenseReport { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Report ) if (-not $OutputPath) { return } $directory = Split-Path -Parent $OutputPath if ($directory -and -not (Test-Path $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } try { if ($OutputFormat -eq "Json") { $Report | ConvertTo-Json -Depth 6 | Out-File -FilePath $OutputPath -Encoding UTF8 -Force } else { $rows = @() foreach ($sku in $Report.Skus) { $rows += [pscustomobject]@{ Type = "Sku" Identifier = $sku.SkuPartNumber Detail = $sku.DisplayName ConsumedSeats = $sku.ConsumedSeats EnabledSeats = $sku.EnabledSeats InactiveUsers = $sku.InactiveUsers UnusedPercentage = $sku.UnusedPercentage } } foreach ($inactive in $Report.InactiveAssignments) { $rows += [pscustomobject]@{ Type = "InactiveAssignment" Identifier = $inactive.User Detail = $inactive.SkuPartNumber ConsumedSeats = 1 EnabledSeats = 1 InactiveUsers = 1 UnusedPercentage = $inactive.DaysInactive } } $rows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Force } Write-Host ("[OK] Rapport geëxporteerd naar {0}" -f $OutputPath) -ForegroundColor Green } catch { Write-Warning "Export mislukt: $($_.Exception.Message)" } } function Invoke-LicenseOptimizationAssessment { [CmdletBinding()] param() $inventory = Get-LicenseInventory $report = Measure-LicenseUtilization -Inventory $inventory Write-LicenseSummary -Report $report Export-LicenseReport -Report $report $findingCount = ([array]$report.Findings).Count if ($findingCount -gt 0) { Write-Host "`n[WARN] Niet alle licenties worden doelmatig benut." -ForegroundColor Yellow } else { Write-Host "`n[OK] Licenties voldoen aan de ingestelde drempels." -ForegroundColor Green } return $report } function Invoke-LicenseOptimizationRemediation { [CmdletBinding()] param() $inventory = Get-LicenseInventory $report = Measure-LicenseUtilization -Inventory $inventory Export-LicenseReport -Report $report Write-Section -Message "Remediatieplan" -Color Cyan if (([array]$report.Findings).Count -eq 0) { Write-Host "Geen directe remediatie nodig. Houd monitoring aan." -ForegroundColor Green return } $step = 1 foreach ($finding in $report.Findings) { Write-Host ("Stap {0}: SKU {1}" -f $step, $finding.SkuPartNumber) -ForegroundColor Yellow Write-Host " - Controleer inkoopcontract en bevestig minimaal benodigd aantal seats." -ForegroundColor Gray Write-Host (" - Converteer inactieve accounts ({0}) naar E3 of verwijder licenties." -f ` $finding.InactiveUsers) -ForegroundColor Gray Write-Host " - Informeer proceseigenaar en leg besluit vast in security board." -ForegroundColor Gray $step++ } } $assessmentReport = $null $criticalFailure = $false try { if ($Assessment) { $assessmentReport = Invoke-LicenseOptimizationAssessment } elseif ($RemediationPlan) { Invoke-LicenseOptimizationRemediation } } catch { Write-Host "[FAIL] Er trad een fout op: $($_.Exception.Message)" -ForegroundColor Red $criticalFailure = $true } finally { Write-Host "`nScript voltooid ($(Get-Date -Format s))." -ForegroundColor Gray } $finalCode = 0 if ($criticalFailure) { $finalCode = 2 } elseif ($Assessment) { $finalCode = if (([array]$assessmentReport.Findings).Count -gt 0) { 1 } else { 0 } } else { $finalCode = 0 } [Environment]::Exit($finalCode)

Risico zonder implementatie

Risico zonder implementatie
Medium: Door licenties ongemoeid te laten blijven miljoenen euro's vastzitten in ongebruikte seats, terwijl compliance-maatregelen ondergefinancierd zijn en auditdiensten het bestuur aanspreken op doelmatigheid.

Management Samenvatting

Stuur licentie-optimalisatie als veiligheidsmaatregel aan. Combineer Microsoft Graph-data, FinOps-kpi's en het script license-optimization.ps1 om ongebruikte seats op te sporen, scenario's te berekenen en besluiten vast te leggen. Herbesteed vrijgekomen budget aan bewezen controls.