Functions/New-AHPolicyExemption.ps1
Function New-AHPolicyExemption { <# .SYNOPSIS Provides a GUI to add policy exemptions for multiple resources of the same type across all subscriptions in the tenant you are currently in. Be aware that it may take a few moments between prompts depending on the number of subscriptions, policy definitions, policy assignments, and resources in your environment. .DESCRIPTION If you have 300 storage accounts across 12 subscriptions that all need an exemption for the same reason, then this makes it easy. If you have a Management Group with a display name of 'Enterprise Policy' then this assumes that you're dealing with policies on that management group. If you don't have a management group with that name then it will prompt you which management group you want to work with. The command then finds which policies and policy sets are assigned at that management group, looks up the corrosponding policy definitions and prompts the user to select which ones the user wants an exclusion for. The command then checks which resources are in the environment and based on that list prompts the user to select which resource type the objects that need to be excluded are. This step just narrows down the choices for when the user needs to select which resources to exclude. The command then gets all resources of the designated type and allows the user to select which ones should be excluded. The command then prompts if the exemption should be a waiver or mitigated. The command then prompts for an expiration date for the exemption. It does not allow exemptions without expiration dates even though Azure does allow it. The command then prompts for a description for the exemption. This is the same exemption even if you selected 300 resources so phrase your description properly. .Example New-AHPolicyExemption .Notes Author: Paul Harrison #> [CmdletBinding()] param() Begin { If ('System.Management.Automation.ServerRemoteDebugger' -eq [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Debugger.GetType().FullName) { throw 'This cmdlet can only be used on a local host and cannot be used from a remote session.' return } elseif ((Get-Item env:/).Name -contains 'AZURE_HTTP_USER_AGENT') { throw 'This cmdlet can only be used on a local host and cannot be used from Azure Cloud Shell.' return } #Region FancyDatePicker Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $dateForm = New-Object Windows.Forms.Form -Property @{ StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen Size = New-Object Drawing.Size 243, 230 Text = 'Select a Date' Topmost = $true } $calendar = New-Object Windows.Forms.MonthCalendar -Property @{ ShowTodayCircle = $false MaxSelectionCount = 1 } $dateForm.Controls.Add($calendar) $okButton = New-Object Windows.Forms.Button -Property @{ Location = New-Object Drawing.Point 38, 165 Size = New-Object Drawing.Size 75, 23 Text = 'OK' DialogResult = [Windows.Forms.DialogResult]::OK } $dateForm.AcceptButton = $okButton $dateForm.Controls.Add($okButton) $cancelButton = New-Object Windows.Forms.Button -Property @{ Location = New-Object Drawing.Point 113, 165 Size = New-Object Drawing.Size 75, 23 Text = 'Cancel' DialogResult = [Windows.Forms.DialogResult]::Cancel } $dateForm.CancelButton = $cancelButton $dateForm.Controls.Add($cancelButton) #EndRegion FancyDatePicker #Region FancyDescriptionInput Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $descriptionForm = New-Object System.Windows.Forms.Form $descriptionForm.Text = 'Data Entry Form' $descriptionForm.Size = New-Object System.Drawing.Size(300, 200) $descriptionForm.StartPosition = 'CenterScreen' $okButton = New-Object System.Windows.Forms.Button $okButton.Location = New-Object System.Drawing.Point(75, 120) $okButton.Size = New-Object System.Drawing.Size(75, 23) $okButton.Text = 'OK' $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK $descriptionForm.AcceptButton = $okButton $descriptionForm.Controls.Add($okButton) $cancelButton = New-Object System.Windows.Forms.Button $cancelButton.Location = New-Object System.Drawing.Point(150, 120) $cancelButton.Size = New-Object System.Drawing.Size(75, 23) $cancelButton.Text = 'Cancel' $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $descriptionForm.CancelButton = $cancelButton $descriptionForm.Controls.Add($cancelButton) $label = New-Object System.Windows.Forms.Label $label.Location = New-Object System.Drawing.Point(10, 20) $label.Size = New-Object System.Drawing.Size(280, 20) $label.Text = 'Please enter the description for the exemption:' $descriptionForm.Controls.Add($label) $textBox = New-Object System.Windows.Forms.TextBox $textBox.Location = New-Object System.Drawing.Point(10, 40) $textBox.Size = New-Object System.Drawing.Size(260, 20) $descriptionForm.Controls.Add($textBox) $descriptionForm.Topmost = $true #EndRegion FancyDescriptionInput } Process { #Steps: ### - Get management group of policies - maybe hard code $ManagementGroup = Get-AzManagementGroup | Where-Object { $_.DisplayName -eq 'Enterprise Policy' } If ($Null -eq $ManagementGroup) { $ManagementGroup = Get-AzManagementGroup | Out-GridView -PassThru -Title 'Select which Management Group has the policy applied to it' } ### - Get Policy Initiative ID - only display policy defintions for policies that are already assigned $PolicySetDefinitionIds = [array]((Get-AzPolicyAssignment -Scope $ManagementGroup.Id -WarningAction SilentlyContinue).Properties.PolicyDefinitionId) #all policy definitions for assigned policies $PolicySets = [array]((Get-AzPolicyAssignment -Scope $ManagementGroup.Id -WarningAction SilentlyContinue)) #all policy definitions for assigned policies #some items are policy definitions, others are policy set definitions, get a readable version of each $PolicySetChoices = $PolicySets | Where-Object { $_.Properties.PolicyDefinitionId -like "*/policySetDefinitions/*" } | Select-Object @{N = 'DisplayName'; E = { $_.Properties.DisplayName } }, @{N = 'Description'; E = { $_.Properties.Description } }, Name, ResourceId, @{N = 'PolicyDefinitionId'; E = { $_.Properties.PolicyDefinitionId } }, * -EA 0 $PolicySetChoices = ForEach ($item in $PolicySetChoices) { $item.Properties.PolicyDefinitionId | ForEach-Object { Get-AzPolicySetDefinition -Id $_ } | Select-Object @{n = 'DisplayName'; E = { $_.Properties.DisplayName } }, @{n = 'Description'; E = { $_.Properties.Description } }, ResourceId } $DefinitionChoices = $PolicySetChoices #+ $PolicyChoices $PolicySetToAddExemptionTo = $DefinitionChoices | Out-GridView -PassThru -Title 'Select which policy to add an exemption to' If (($PolicySetToAddExemptionTo.gettype()).BaseType.Name -eq 'Array') { throw 'One and only one policy set may be selected' return } $PolicyAssignment = (Get-AzPolicyAssignment -Scope $ManagementGroup.Id -PolicyDefinitionId $PolicySetToAddExemptionTo.ResourceId -WarningAction SilentlyContinue) ### - Get Policy definition within the initiative If ($PolicySetToAddExemptionTo.ResourceId -like "*/policyDefinitions/*") { $PolicyDefinitionToExclude = $PolicySetToAddExemptionTo.ResourceId #$policyToAddExemptionTo = (Get-AzPolicyAssignment -Scope $ManagementGroup.Id -PolicyDefinitionId $PolicySetToAddExemptionTo.ResourceId) } Else { #it is a policy set so we need to know which policies within the set to exempt $definitionLookup = Get-AzPolicyDefinition | ForEach-Object { @{$_.PolicyDefinitionId = $_.Properties.DisplayName } } # we want pretty information so that humans can decide what to do $PolicyDefinitionToExclude = (Get-AzPolicySetDefinition -ResourceId $PolicySetToAddExemptionTo.ResourceId).Properties.PolicyDefinitions | Select-Object @{n = 'DefinitionDisplayName'; E = { $definitionLookup."$($_.PolicyDefinitionId)" } }, * -EA 0 | Out-GridView -PassThru -Title 'Select which policy/policies to add exemptions to within the initiative' } ### - Get Resource(s) to exclude $SubscriptionsInThisTenant = Get-AzSubscription -TenantId (Get-AzContext).Tenant.id $AllResourceTypesScriptBlock = { (Get-AzResource | Group-Object ResourceType).Name } $allResourceTypes = Invoke-AzureCommand -ScriptBlock $AllResourceTypesScriptBlock -Subscription $SubscriptionsInThisTenant | Select-Object -Unique $resourceTypeToExclude = $allResourceTypes | Out-GridView -PassThru -Title 'Select which resource type to exclude' $resourcesToExcludeScriptBlock = { $resourceTypeToExclude | ForEach-Object { Get-AzResource -ResourceType $_ } } $resourcesToExcludeAll = Invoke-AzureCommand -ScriptBlock $resourcesToExcludeScriptBlock -Subscription $SubscriptionsInThisTenant $resourcesToExclude = $resourcesToExcludeAll | Out-GridView -PassThru -Title 'Select which specific resources to exclude' #get category $ExemptionCategory = @('Waiver', 'Mitigated') | Out-GridView -PassThru -Title 'Select the Exemption Category' #get expiration date $result = $dateForm.ShowDialog() If ($result -eq [Windows.Forms.DialogResult]::OK) { $ExpirationDate = $calendar.SelectionStart.ToString('yyyy-MM-dd') } Else { throw 'A date must be selected' return } If ($calendar.SelectionStart -gt [datetime]::now.AddDays(366)) { Write-Warning 'This exemption will be processed however expiration dates over 1 year in the future are discouraged.' } #get description $descriptionForm.Add_Shown({ $textBox.Select() }) $result = $descriptionForm.ShowDialog() if ($result -eq [System.Windows.Forms.DialogResult]::OK) { $description = $textBox.Text } Else { throw 'a description must be entered' return } ### - Create splats $MySplats = ForEach ($definitionToExclude in $PolicyDefinitionToExclude) { ForEach ($resource in $resourcesToExclude) { $Policy = Get-AzPolicyDefinition -Id $definitionToExclude.PolicyDefinitionId $PolicyName = $Policy.Properties.DisplayName $DisplayName = "$($resource.Name) - $($PolicyAssignment.Properties.DisplayName) - $($Policy.Properties.DisplayName)" #$DisplayName = $DisplayName.Substring(0, 127).Trim() $DisplayName = $DisplayName.Substring(0, $(If ($DisplayName.Length -lt 128) { $DisplayName.Length }Else { 128 })).Trim() $ExemptionName = $DisplayName.Substring(0, $(If ($DisplayName.Length -lt 64) { $DisplayName.Length }Else { 64 })).Trim() @{ Name = $ExemptionName.replace('%', '').replace('&', '').replace('/', '').replace('?', '').replace('\', '').replace('<', '').replace('>', '').replace(':', '') #Azure doesn't like these characters PolicyAssignment = $PolicyAssignment Scope = $resource.ResourceId ExemptionCategory = $ExemptionCategory DisplayName = $DisplayName.replace('%', '').replace('&', '').replace('/', '').replace('?', '').replace('\', '').replace('<', '').replace('>', '').replace(':', '') PolicyDefinitionReferenceId = $definitionToExclude.policyDefinitionReferenceId ExpiresOn = $ExpirationDate description = $description } } } ### - Create exclusion ForEach ($splat in $MySplats) { New-AzPolicyExemption @splat } } end {} } |