De basis van Purview heb ik eerst zelf in mijn eigen tenant ingericht. Toen ik hetzelfde bij klanten ging doen, schreef ik de labels, policies en regels uit in JSON en bouwde ik er PowerShell omheen, zodat elke uitrol op dezelfde manier zou gaan. Dat heb ik nu openbaar gemaakt als purview-as-code, zodat anderen het ook kunnen gebruiken: een PowerShell-module die sensitivity labels, label policies, DLP en retentie uitrolt vanuit JSON. Voordat ik het openbaar maakte heb ik het grondig nagelopen, en daarbij vond ik twee bugs die er vanaf het begin in zaten. Door een daarvan deed het veiligst ogende commando uit de README precies het tegenovergestelde van wat het beloofde. Dat goed oplossen werd een herbouw, en toen ik die tegen een echte tenant testte, kwamen er nog twee boven.
Foto: Getty Images via Unsplash
Wat het project doet
Invoke-PurviewDeployment controleert eerst of je sessie verbonden is met de tenant uit de config. Daarna leest het de tenant uit, vergelijkt elk object met de config en toont een plan: wat er wordt aangemaakt, wat er verandert (met de oude en de nieuwe waarde), wat al klopt, en, voor sensitivity labels, gepubliceerde labels en locaties, wat alleen in de tenant bestaat. Met -WhatIf stopt het daar. Zonder -WhatIf voert het alleen de verschillen door, dus een tweede run op een ongewijzigde tenant verandert niets. Test-PurviewConfig controleert de JSON tegen een schema, plus wat een schema niet kan controleren, nog voordat er een verbinding is.Twee hulpscripts regelen wat eromheen hoort: de Conditional Access-policy voor app enforced restrictions (zonder die policy doet de instelling voor onbeheerde apparaten op een containerlabel niets) en standaardbewaarlabels op SharePoint-bibliotheken.
Het verwijdert geen labels of policies. Een label uit een label policy halen, of een locatie uit een policy, gebeurt alleen met
-AllowRemoval. Zonder die parameter staat het verschil als drift in het plan en blijft het een beslissing voor een mens. Een -WhatIf die niets tegenhield
-WhatIf moest draaien, om te zien wat er zou veranderen. Het deployscript ondersteunde ShouldProcess, en elke functie in de modules controleerde $PSCmdlet.ShouldProcess voordat er iets veranderde. Dat ziet er goed uit, en zo zouden de meeste mensen het schrijven.Het werkt niet. Preference variables zoals
$WhatIfPreference lopen niet door van een script naar een scriptmodule. Een module heeft een eigen session state, dus als het script een modulefunctie aanroept zonder -WhatIf expliciet mee te geven, zoekt de functie $WhatIfPreference op in de scope van de module en de globale scope, vindt $false, en gaat gewoon door. Daar komt bij dat Security & Compliance PowerShell -WhatIf zelf negeert; de cmdlet-documentatie zegt dat ronduit. Een tweede vangnet is er dus ook niet.Zo reproduceer je het in een paar regels:
# Demo.psm1
function Set-Thing {
[CmdletBinding(SupportsShouldProcess)]
param()
if ($PSCmdlet.ShouldProcess("tenant", "Change setting")) { "CHANGED: this ran for real" }
}
# Deploy.ps1
[CmdletBinding(SupportsShouldProcess)]
param()
Import-Module (Join-Path $PSScriptRoot "Demo.psm1") -Force
Set-Thing
.\Deploy.ps1 -WhatIf en je krijgt "CHANGED: this ran for real". Verander de laatste regel in Set-Thing -WhatIf:$WhatIfPreference en je krijgt de verwachte "What if: Performing the operation...". Binnen de module gaan aanroepen tussen functies wel goed, omdat ze de scope van de module delen; alleen bij de overgang van script naar module gaat de instelling verloren.Om te zien wat het in het echte project deed, heb ik de Purview-cmdlets vervangen door nepversies die elke aanmaak- en wijzigingsaanroep tellen, en de uitrol met
-WhatIf gedraaid op een van de voorbeeldconfigs. De oorspronkelijke versie voerde 31 wijzigingen uit. Door -WhatIf:$WhatIfPreference expliciet aan elke modulefunctie mee te geven, waren het er nul. Dat was de snelle oplossing. Een validatiescript dat nooit iets vond
Test-PurviewConfig.ps1 verzamelde fouten en waarschuwingen in $errors en $warnings, en controleerde labels in een geneste functie, zodat die sublabels recursief kon doorlopen. In die functie voegt $errors += "..." niets toe aan de array van het script: het maakt een nieuwe lokale variabele met een kopie. Als de functie klaar is, is de kopie weg. Elke labelcontrole (ontbrekende weergavenamen, dubbele namen, encryptie zonder protection type) draaide, vond problemen, en gooide ze weg. Een config met een dubbele labelnaam kwam door de validatie.De oplossing was de scope-modifier
$script: op die variabelen. Een klassieke PowerShell-valkuil, en een goede reden om een validatiescript ook te testen met een config die zou moeten falen, niet alleen met een die zou moeten slagen. Van snelle oplossing naar een plan
-WhatIf keek de uitrol nooit eerst wat er al in de tenant stond, en daar kwamen nog een paar kleinere bugs uit voort. Labels werden alleen ingesteld als ze werden aangemaakt; bij latere runs meldde het script "already exists" en ging door. DLP-regels met BlockAccess op false werden overgeslagen, omdat de controle false als "niet ingesteld" zag. De locaties van een bestaande DLP-policy wijzigen mislukte, omdat Set-DlpCompliancePolicy geen -ExchangeLocation of -SharePointLocation heeft; daarvoor kent het alleen de Add- en Remove-varianten. En een retentiebeleid waarvan de regel niet kon worden aangemaakt, werd bij latere runs nooit hersteld.Versie 0.2.0 is daarom een module die eerst leest en dan pas schrijft. Elke wijziging komt eerst in het plan en loopt daarna door één functie die
ShouldProcess controleert. Omdat je de modulefunctie zelf aanroept, is er geen overgang van script naar module meer waar -WhatIf kan verdwijnen. Pester-tests draaien de module tegen nagebootste Security & Compliance-cmdlets en controleren dat een -WhatIf-run niets aanroept dat de tenant wijzigt, en dat een tweede run na een uitrol ook niets meer aanroept. GitHub Actions draait ze bij elke push naar main en bij elke pull request.Tenants die vanaf 1 oktober 2025 zijn aangemaakt, gebruiken label groups in plaats van parent labels, dus het project ondersteunt
"IsLabelGroup": true. Dat deel heb ik in een echte tenant getest: New-Label -IsLabelGroup maakt de groep aan, een label dat je aanmaakt met -ParentId op de GUID daarvan komt erin te staan, en de groep zelf publiceren faalt. Dat laatste vangt de validatie af voordat je gaat uitrollen. Wat een echte tenant aan het licht bracht
-WhatIf, daarna een config met twee testlabels die de module echt aanmaakte en wijzigde. De labels zijn nooit gepubliceerd en een script heeft ze achteraf weer verwijderd. DLP-policies, label policies en bewaarlabels zijn alleen gepland tegen de tenant, niet aangemaakt.Het meeste hield stand. Het plan klopte met wat er in de tenant stond, tot en met een koptekst die wel bestond maar uit stond. Na het aanmaken van de testlabels, en na het wijzigen van een ervan, meldde de volgende run geen wijzigingen. Purview bewaart labelinstellingen als sleutel-waardeparen in
LabelActions, en de sleutelnamen kon ik nergens gedocumenteerd vinden; de test bevestigde elke sleutel die de module leest.Twee dingen hielden geen stand:
Get-RetentionCompliancePolicygeeft alleen betrouwbare locaties terug als je-DistributionDetailmeegeeft. Dat staat in de documentatie; zo ver had ik niet gelezen. Een bestaand retentiebeleid met Exchange op All kwam daardoor als wijziging in het plan.- Een label zonder scope geldt voor bestanden en e-mail, en voor die combinatie weigert
Set-Labeldoor de gebruiker gedefinieerde encryptie, tenzijPromptUseren daarnaastDoNotForwardofEncryptOnlyaan staan. Mijn voorbeelden zetten alleenPromptUser. Het label werd aangemaakt, de encryptiestap mislukte, en de module meldde dat als mislukte wijziging, zoals het hoort.
De openbare versie bevat geen tenantnamen, klantconfiguraties of site-URL's; de voorbeelden gebruiken Contoso.
Wat ik heb geleerd
Schrijf je PowerShell-modules met
SupportsShouldProcess, test -WhatIf dan vanuit het script dat ze aanroept, niet alleen vanuit de modulefunctie zelf. Werkt je tool met Security & Compliance PowerShell, reken er dan niet op dat de cmdlets je beschermen: de enige -WhatIf daar is degene die je zelf bouwt. En draai het tegen een echte tenant voordat je de nagebootste cmdlets vertrouwt, want die zijn het per definitie met je eens.Wil je het proberen? Clone de repo, kopieer een van de voorbeelden naar een nieuwe map
configs, zet je eigen tenantdomein erin, en draai: Import-Module ./PurviewAsCode
Test-PurviewConfig -ConfigFile ./configs/jouwconfig.json
Connect-IPPSSession
Invoke-PurviewDeployment -ConfigFile ./configs/jouwconfig.json -UseExistingSession -WhatIf
Bronnen
- purview-as-code op GitHub
- about_Preference_Variables (Microsoft Learn)
- about_Scopes: modules and session state (Microsoft Learn)
- Set-Label: the WhatIf switch doesn't work in Security & Compliance PowerShell (Microsoft Learn)
- Get-RetentionCompliancePolicy: -DistributionDetail (Microsoft Learn)
- Migrate parent sensitivity labels to label groups (Microsoft Learn)