Create-AzDiagPolicy.ps1
<#PSScriptInfo
.VERSION 2.9 .GUID e0962947-bf3c-4ed4-be3b-39cb7f6348c6 .AUTHOR jbritt@microsoft.com .COMPANYNAME Microsoft .COPYRIGHT Microsoft .TAGS .LICENSEURI .PROJECTURI https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES September 07, 2021 2.9 Update * Updated the API version for both below types. This was recently caught with the help of ARM TTK: https://github.com/Azure/arm-ttk indicating an old version of an API for Azure Policy within the ARM template that is generated as part of this script. "type": "Microsoft.Authorization/policyDefinitions", "apiVersion": "2020-09-01" and "type": "Microsoft.Authorization/policySetDefinitions", "apiVersion": "2020-09-01" **NOTE**: Previous API version leveraged was 2019-09-01 #> <# .SYNOPSIS Create a custom policy (and optional Policy Initative) to enable Azure Diagnostics and send that data to a Log Analytics Workspace, Regional Storage Account or Regional Event Hub Note This script currently supports onboarding Azure resources that support Azure Diagnostics (metrics and logs) to Log Analytics, Event Hub, and Storage .DESCRIPTION This script takes a SubscriptionID, ResourceType, ResourceGroup as parameters, analyzes the subscription or specific ResourceGroup defined for the resources specified in $Resources, and builds a custom policy for diagnostic metrics/logs for Event Hubs, Storage and Log Analytics as sink points for selected resource types. .PARAMETER ManagementGroupDeployment Leverage this switch to export the ARM template for your policy initiative to support Management Group as a scope target. This will place all resources (Custom Policies and Policy Initiative) in the same MG upon deployment via "New-AzManagementGroupDeployment" Ex: New-AzManagementGroupDeployment -Name DiagAzurePolicyInit -ManagementGroupId CatDev -Location eastus -TemplateFile .\MgTemplateExportMG.json -ManagementGroupDeployment -TargetMGID CatDev .PARAMETER Environment The cloud environment that you are needing to analyze. Default is AzureCloud Available clouds: AzureChinaCloud, AzureCloud,AzureGermanCloud,AzureUSGovernment .PARAMETER SubscriptionId The subscriptionID of the Azure Subscription that contains the resources you want to analyze .PARAMETER ResourceType The ResourceType you want to create a policy for within your Azure Subscription .PARAMETER ResourceGroupName If desired, use a resourcegroup instead of analyzing all resources within an Azure subscription .PARAMETER ResourceName If desired, use a resource name instead of analyzing all resources within an Azure subscription .PARAMETER ExportDir Directory to export policies to. If not used, the working directory of the script will be leveraged as the export directory base folder .PARAMETER ExportAll Parameter used to bypass the prompt for resource types to export policies for. THis parameter if used will export all resource type policies .PARAMETER ExportLA Export only Log Analytics policies for Azure Diagnostics .PARAMETER ExportEH Export only Event Hub Policies for Azure Diagnostics .PARAMETER ExportStorage Export only Storage Policies for Azure Diagnostics .PARAMETER ValidateJSON If specified will do a post export validation recursively against the export directory or will validate JSONs recursively in current script directory and subfolders or exportdirectory (if specified). .PARAMETER Tenant Use the -Tenant parameter to bypass the subscriptionID requirement Note: Cannot use in conjunction with -SubscriptionID .PARAMETER LogPolicyOnly Use the -LogPolicyOnly parameter to export Azure Policies for resourceTypes that support Logs (bypass those that only support Metrics) .PARAMETER AllRegions This AllRegions switch can be used to bypass the "location" check / parameter in the Azure Policies for Log Analytics. Note: This switch does not support EventHub/Storage based policies due to the region requirement for EventHubs/Storage and Azure Diagnostic settings .PARAMETER ManagementGroup This ManagementGroup switch can be used to change scope for scanning for resourceTypes that suppport Azure Diags to be at the Management Group .PARAMETER ManagementGroupID This parameter can be provided along with the ManagementGroup switch to predefine which MG you want to scan. If this parameter is not provided the list of Management Groups you have access to will be presented in a menu you can then select. .PARAMETER ExportInitiative This ExportInitiative Switch determines if you are exporting a Policy Initiative (ARM Template) or just raw policy files .PARAMETER InitiativeDisplayName This parameter allows you to set the Policy Initiative Name so you can have two Initiatives with slighly different names leveraging the same Custom Policies underneath. Not the policy names and Initiatives are hash values according to the display name leveraged and the sink point used. Not the display name is limited to 127 chars (see https://aka.ms/AzureLimits) .PARAMETER TemplateFileName This parameter allows you to determine the outputted ARM template file name for your policy initiative. This can be useful when leveraged with an ADO pipeline and test automation to validate policy drift according to a baselined exported Policy Initiatve that has been promoted into production versus what your environment states it should be configured as (at current state of running the script) .PARAMETER ADO This parameter allows you to run this script in Azure DevOps pipeline utilizing an SPN (no op - deprecated) .PARAMETER Dedicated This parameter allows you to specify a dedicated table for Azure Diagnostics for those ResourceTypes that support it .EXAMPLE .\Create-AzDiagPolicy.ps1 -SubscriptionId "fd2323a9-2324-4d2a-90f6-7e6c2fe03512" -ResourceType "Microsoft.Sql/servers/databases" -ResourceGroup "RGName" -ExportLA -ExportEH Take in parameters and execute silently without prompting against the scope of a resourceType, Resource Group, with a specified subscriptionID as scope .EXAMPLE .\Create-AzDiagPolicy.ps1 -Dedicated -SubscriptionId "fd2323a9-2324-4d2a-90f6-7e6c2fe03512" -ResourceType "Microsoft.Sql/servers/databases" -ResourceGroup "RGName" -ExportLA -ExportEH Same as above but will allow for dedicated table for Log Analytics specific policies exported (for those ResourceTypes that support it). Note: This is a blanket setting when used with a Policy Initiative Export. The setting will only enabled dedicated support for those resourceTypes that support it. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportLA Will prompt for subscriptionID to leverage for analysis, prompt for which resourceTypes to export for policies, and export the policies specific to Log Analytics only .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportEH Will prompt for subscriptionID to leverage for analysis, prompt for which resourceTypes to export for policies, and export the policies specific to Event Hub only .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportStorage Will prompt for subscriptionID to leverage for analysis, prompt for which resourceTypes to export for policies, and export the policies specific to Storage account only .EXAMPLE .\Create-AzDiagPolicy.ps1 -ValidateJSON -ExportDir "EH-Policies" Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportEH -ExportLA -ValidateJSON -Tenant -ExportDir ".\LogPolicies" Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors. This example also provides the ability to go against the entire Azure AD Tenant as opposed to a single subscription .EXAMPLE .\Create-AzDiagPolicy.ps1 -LogPolicyOnly -ExportAll -ExportEH -ExportLA -ValidateJSON -Tenant -ExportDir ".\LogPolicies" Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors. This example also provides the ability to go against the entire Azure AD Tenant as opposed to a single subscription. Exports Log Policies (metric check is bypassed) .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportEH -ExportLA -ValidateJSON -ExportDir ".\LogPolicies" -Tenant -AllRegions Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors. This example also allows for bypassing the location specific requirements for the exported Log Analytics policies. The scope for this export will be at the Azure AD tenant level. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportEH -ExportLA -ValidateJSON -ExportDir ".\LogPolicies" -ManagementGroup -AllRegions Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors. This example also allows for bypassing the location specific requirements for the exported Log Analytics policies. The scope for this export will be at the Management Group level. If ManagementGroupID is left off, a menu will be provided during execution of the script to select one. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportLA -ValidateJSON -ExportDir ".\LogPolicies" -ManagementGroup -AllRegions -ExportInitiative Will leverage the specified export directory (relative to current working directory of PS console or specify fully qualified directory) and will validate all JSON files to ensure they have no syntax errors. This example also allows for bypassing the location specific requirements for the exported Log Analytics policies. The scope for this export will be at the Management Group level. If ManagementGroupID is left off, a menu will be provided during execution of the script to select one. Finally, this example provides the ability to take the custom policies and write them to an ARM template Policy Initiative. Note: you can only provide -ExportLA or -ExportEH (not both) as the policy initiative requires unique parameters on assignment to coincide with the sink point you are leveraging. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportLA -ValidateJSON -ExportDir ".\LogPolicies" -ManagementGroup -AllRegions -ExportInitiative -InitiativeDisplayName "Azure Diagnostics Policy Initiative for a Log Analytics Workspace" -TemplateFileName 'ARMTemplateExport' Similar to the previous example, this one adds additional capability of allowing you to define the display name for the Policy Initiative as well as predetermine the templatefile name for the Policy Initiative. Note the display name is validated that it is less than 127 chars long if provided. Script will break if that value is either exceeded of the value is less than 1 char. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportAll -ExportStorage -ValidateJSON -ExportDir ".\LogPolicies" -ManagementGroup -AllRegions -ExportInitiative -InitiativeDisplayName "Azure Diagnostics Policy Initiative for a Regional Storage Account" -TemplateFileName 'ARMTemplateExport' Same as previous example, but exporting to a storage account as a sink point. .EXAMPLE .\Create-AzDiagPolicy.ps1 -Environment AzureUSGovernment -ExportAll -ExportStorage -ValidateJSON -ExportDir ".\LogPolicies" -ManagementGroup -AllRegions -ExportInitiative -InitiativeDisplayName "Azure Diagnostics Policy Initiative for a Regional Storage Account" -TemplateFileName 'ARMTemplateExport' Same as previous example, but leveraging Azure Government Cloud. .EXAMPLE .\Create-AzDiagPolicy.ps1 -ExportDir .\LogPolicies -ExportAll -ExportLA -ExportInitiative -TemplateFileName MgTemplateExportMG -ManagementGroupDeployment -AllRegions Exports an ARM Template Policy Initiative supporting a Management Group supporting all Logs for Log Analytics and all regions supported NOTE: Use the following example to deploy this template to a target management group New-AzManagementGroupDeployment -Name DiagAzurePolicyInit -ManagementGroupId MyMGID -Location eastus -TemplateFile .\MgTemplateExportMG.json -TargetMGID MyMGID .NOTES AUTHOR: Jim Britt Principal Program Manager - Azure CXP API (Azure Product Improvement) LASTEDIT: September 07, 2021 2.9 Minor Update * Updated the API version for both below types. This was recently caught with the help of ARM TTK: https://github.com/Azure/arm-ttk indicating an old version of an API for Azure Policy within the ARM template that is generated as part of this script. "type": "Microsoft.Authorization/policyDefinitions", "apiVersion": "2020-09-01" and "type": "Microsoft.Authorization/policySetDefinitions", "apiVersion": "2020-09-01" **NOTE**: Previous API version leveraged was 2019-09-01 May 05, 2021 2.8 Updates * Adding the ability to leverage a dedicated table (instead of AzureDiagnostics) for resourceTypes that support it Reference Link: https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-manager-diagnostic-settings#diagnostic-setting-for-recovery-services-vault THANK YOU Eric Golpe (Principal Cloud Solution Architect) - Microsoft for leaning in and driving this change on behalf of a customer requirement. Note: Utilize -Dedicated switch to enable. This will be a blanket configuration for all policies and will only enable for those resourceTypes that support it. Otherwise, the default will be AzureDiagnostics table Feb 12, 2021 2.7 Minor updates * Huge thanks to Panagiotis Tsoukias (https://github.com/ptsouk) Customer Engineer at Microsoft for fixing the following * Fixed some missing logic for Management Groups in PolicyID logic * Another huge thanks to Nikolay Sucheninov and the VIAcode team for fixing the following issues raised by ARM TTK * Fixed schema URLs to use https * Removed redundant dependsOn logic that was not necessary or even functional *** Thank you for supporting the script and community effort around this solution - everyone benefits! **** November 11, 2020 2.6 Fixed more issues with REST API logic due to updates to Az cmdlets November 03, 2020 2.5 Fixed a bug with REST API logic October 30, 2020 2.4 Added parameter -ManagementGroupDeployment for ARM Export This parameter switch provides the option to export an ARM Template Policy Initiative that supports a Management group target scope. Special Thanks to Kristian Nese (https://github.com/krnese) for my sounding board and using his big brain to work through some of the ARM goo. Thank you Kamil (https://github.com/kwiecek) to for pushing for this feature to help improve the experience for our customersk leveraging it and actively collaborating on improving our final enhancement! Thank you Dimitri Lider (https://github.com/dimilider) for the additional collaboration and also looking out for improving this script! Changed REST API Token creation due to a recent breaking change I observed where the old way no longer worked. If you have any issues with this change, please let me know here on Github (https://aka.ms/AzPolicyScripts) August 13, 2020 2.3 Added parameter -ADO This parameter provides the option to run this script leveraging an SPN in Azure DevOps. Special Thanks to Nikolay Sucheninov and the VIAcode team for working to get these scripts integrated and operational in Azure DevOps to streamline "Policy as Code" processes with version drift detection and remediation through automation! August 03, 2020 2.2 Environment Added to script to allow for other clouds beyond Azure Commercial AzureChinaCloud, AzureCloud,AzureGermanCloud,AzureUSGovernment Special Thanks to Michael Pullen for your direct addition to the script to support additional Azure Cloud reach for this script! :) Thank you Matt Taylor, Paul Harrison, and Abel Cruz for your collaboration in this area to debug, test, validate, and push on getting Azure Government supported with these scripts! Fixed Bug with "Kind" and not exporting all policies for ResourceProviders that leverage Kind with same RP (ex: Azure SQL DB, Azure SQL DW) Special Thanks to Mo Barry for helping me isolate this bug in the script July 16, 2020 2.1 Storage Added as a sink to policy and policy initiative ARM template exports June 07, 2020 2.0 Significant Updates this version which pushed it to 2.0! * Special thanks to Dimitri Lider, and Julian Hayward (Microsoft) once again for their constant inputs on this script to improve! * A HUGE THANK YOU to the ClearDATA crew for their support in bug bashing an early iteration of this 2.0 ver script before going broadly. -- Thank you Jason Singh for leaning in on support for prioritizing review of this effort -- And for Rob Sanders (https://github.com/rwsanders) for his isolation of a breaking bug I introduced that was much more easily isolated and resolved with his support! * Another special call out to Kristian Nese @KristianNese (https://github.com/krnese) and Chris Eggert (https://github.com/pilor) @pilor * for their technical review and big brain guidance related to approach and technical accuracy in the area of ARM and Policy! Policy Initiative Support - Added support for exporting to ARM Template Policy Initiative Artifact -- Option for customized displayname for Initiative -- Ability for Custom Azure Policies and Initiative to be idempotent due to creating a unique name via hash --- Inspiration reference http://xpertkb.com/compute-hash-string-powershell/ User Experience - Added logic to return to initial subscription context after successfully running the script (useful on Tenant / Management Group analysis) - Improved token expiration experience for Azure Auth - Added Total Execution Time to help understand performance of script against environment - Updated Examples Visual Updates - Prettify function added to clean up JSON export -- Inspiration via reference article / source: https://stackoverflow.com/questions/24789365/prettify-json-in-powershell-3/61988399#61988399 - Added process to clean up exported JSON -- Credit to https://github.com/DeadPoolHeartsRR for their input on another script to use logic to use regex to clean up non printable chars (thank you! - Dead Pool Rocks BTW :) ) Feature Enhancement - Added "Kind" to evaluation to support RPs that leverage kind in category evaluation (Example: Azure SQL DW vs Azure SQL DB) November 27, 2019 1.4 - Updated RoleDefinitionID for Log Analytics based policies to be "Log Analytics Contributor" - Special thanks to Dimitri Lider, and Julian Hayward (Microsoft) for their input on this update! Keep the ideas coming! :) - Added Parameter Sets to initial parameters to refine experience - Added an option for Management Group as a scope providing a bit more flexiblity when it comes to scanning for resourceTypes supporting Diags. - Special thanks to Sam El-Anis (Microsoft) https://twitter.com/SamElAnis for the idea on this one! October 23, 2019 1.3 - Added parameter for "all locations" for Log Analytics based policies - Special thanks to Dimitri Lider (Microsoft) for his input on this feature! Keep the ideas coming! :) August 21, 2019 1.2 - Improved efficiency for skipping invalid resources on analysis - Added Tenant to bypass subscription listing and go against all subs in current AD tenant - Added LogPolicyOnly switch to only export Azure Policies for resources that support Logs (metrics bypassed) - Special thanks to Dimitri Lider (Microsoft) for his contributions to the 2nd and 3rd bullet above Thank you for providing feedback Dimitri! June 06, 2019 Updated a parameter for Event Hub name causing issues with configuration of Diagnostic Settings April 29, 2019 Initial Special thanks to John Kemnetz @jkemnetz (https://twitter.com/jkemnetz) for his initial project here that I based some of my array logic off of: https://github.com/johnkemnetz/azmon-onboarding/tree/master/policies Thanks to Tao Yang @MrTaoYang (https://twitter.com/MrTaoYang) for his collaboration initially and his hard work he has done here: https://blog.tyang.org/2018/11/19/configuring-azure-resources-diagnostic-log-settings-using-azure-policy/ that he has created to support our community in this space! And thank you Nick Kiest from the Azure Monitor Product Team for supporting the project from their side seeing value in the effort! Finally Kristian Nese @KristianNese (https://twitter.com/KristianNese) from AzureCAT for his direct support on feedback as I navigated this effort and for him providing technical expertize in this space. .LINK This script posted to and discussed at the following locations: https://aka.ms/AzPolicyScripts #> [cmdletbinding( DefaultParameterSetName='Default' )] param ( # Environment defines what cloud you are analyzing (defaults to AzureCloud) [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Export')] [ValidateSet("AzureChinaCloud","AzureCloud","AzureGermanCloud","AzureUSGovernment")] [string]$Environment = "AzureCloud", # Environment defines what cloud you are analyzing (defaults to AzureCloud) # Use this switch to enable the script to run via SPN in an Azure DevOps Pipeline [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Export')] [switch]$ADO = $False, # Default of $False assumes subscription as target - if $True will modify intiative properties to support MG target [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Export')] [switch]$ManagementGroupDeployment=$False, # Export Directory Path for Artifacts - if not set - will default to script directory [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Export')] [string]$ExportDir, # Export all policies without prompting - default is false [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [switch]$ExportAll=$False, # Export Event Hub Specific Policies [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [switch]$ExportEH=$False, # Export Log Analytics Specific Policies [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [switch]$ExportLA=$False, # Export Storage Specific Policies [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [switch]$ExportStorage=$False, # Provide SubscriptionID to bypass subscription listing [Parameter(Mandatory=$False)] [Parameter(ParameterSetName='Subscription')] [string]$SubscriptionId, # Tenant switch to bypass subscriptionId requirement [Parameter(Mandatory=$False)] [Parameter(ParameterSetName='Tenant')] [switch]$Tenant=$False, # Management Group switch to allow for scanning all subs in a management group (instead of tenant wide or sub only) [Parameter(Mandatory=$False)] [Parameter(ParameterSetName='ManagementGroup')] [switch]$ManagementGroup=$False, # Management Group ID to scan (if left blank - will build list and prompt for selection if $ManagementGroup switch is used) [Parameter(Mandatory=$False)] [Parameter(ParameterSetName='ManagementGroup')] [string]$ManagementGroupID, # Validate all exported policies to ensure they are proper JSON [Parameter(Mandatory=$False)] [switch]$ValidateJSON=$False, # Switch to determine if you are going to export an ARM Initiative or all policy files. Default is all policy files unless this switch is used [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Initiative')] [switch]$ExportInitiative=$False, # Specify a policy initiative display name (default will be used otherwise) [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Initiative')] [ValidateLength(1,127)] [string]$InitiativeDisplayName, # Specify your output file name for the ARM Template Policy Initiative. If not used, ARM-Template-azurepolicyinit.json will be used [Parameter(ParameterSetName='Default',Mandatory = $False)] [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [Parameter(ParameterSetName='Initiative')] [string]$TemplateFileName, # When switch is used, only Azure Policies to capture logs will be exported (metric only resources bypassed) [switch]$LogPolicyOnly=$False, # AllRegions switch to allow log Analytics to use all regions instead of being region sensitive [switch]$AllRegions=$False, # Dedicated switch to allow log Analytics to use a dedicated table instead of AzureDiagnostics table (if supported) [switch]$Dedicated=$False, # Add ResourceType to reduce scope to Resource Type instead of entire list of resources to scan [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [string]$ResourceType, # Add a ResourceGroup name to reduce scope from entire Azure Subscription to RG [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [string]$ResourceGroupName, # Add a ResourceName name to reduce scope from entire Azure Subscription to specific named resource [Parameter(ParameterSetName='Export')] [Parameter(ParameterSetName='Subscription')] [Parameter(ParameterSetName='Tenant')] [Parameter(ParameterSetName='ManagementGroup')] [string]$ResourceName ) # FUNCTIONS # Build out the body for the GET / PUT request via REST API function BuildBody ( [parameter(mandatory=$True)] [string]$method ) { $BuildBody = @{ Headers = @{ Authorization = "Bearer $($token.AccessToken)" 'Content-Type' = 'application/json' } Method = $Method UseBasicParsing = $true } $BuildBody } # Get the ResourceType listing from all ResourceTypes capable in this subscription # to be sent to log analytics - use "-ResourceType" param to bypass function Get-ResourceType ( [Parameter(Mandatory=$True)] [array]$allResources, [Parameter(Mandatory=$False)] [array]$analysis ) { If(!($analysis)) { $analysis = @() } $GetScanDetails = @{ Headers = @{ Authorization = "Bearer $($token.AccessToken)" 'Content-Type' = 'application/json' } Method = 'Get' UseBasicParsing = $true } foreach($resource in $allResources) { $Invalid = $false $Categories =@(); $metrics = $false #initialize metrics flag to $false $logs = $false #initialize logs flag to $false #Establish URI to gather resources # Determine cloud and ensure proper REST Endpoint defined $azEnvironment = Get-AzEnvironment -Name $Environment $URI = "$($azEnvironment.ResourceManagerUrl)$($Resource.ResourceId.substring(1))/providers/microsoft.insights/diagnosticSettingsCategories/?api-version=2017-05-01-preview" #Write-Host "URI: $($URI)" $Exists = $false if($Analysis) { foreach($A in $Analysis) { if($($Resource.resourceType -eq $A.resourcetype) -and $($Resource.Kind -eq $A.Kind)) { $exists = $True } } } if (!($Exists)) { try { Write-Verbose "Checking $($resource.ResourceType)" Try { $Status = Invoke-WebRequest -uri $URI @GetScanDetails } catch { # Uncomment below to see actual error. Certain resources are not ResourceTypes that can support Logs and Metrics so the host error is being muted #write-host $Error[0] -ForegroundColor Red $Invalid = $True $Logs = $False $Metrics = $False $ResponseJSON = '' } if(!($Invalid)) { $ResponseJSON = $Status.Content|ConvertFrom-Json -ErrorAction SilentlyContinue } # If logs are supported or metrics on each resource, set value as $True If($ResponseJSON) { foreach($R in $ResponseJSON.value) { if($R.properties.categoryType -eq "Metrics") { $metrics = $true } if($R.properties.categoryType -eq "Logs") { $Logs = $true $Categories += $r.name } } $Kind = $Resource.kind } } catch {} finally { $object = New-Object -TypeName PSObject -Property @{'ResourceType' = $resource.ResourceType; 'Metrics' = $metrics; 'Logs' = $logs; 'Categories' = $Categories; 'Kind' = $Kind} $analysis += $object } } } # Return the list of supported resources # Add the "ALL" option to the tail of the analysis array if we are only going against one subscription if($SubscriptionId) { $object = New-Object -TypeName PSObject -Property @{'ResourceType' = "All"; 'Metrics' = "True"; 'Logs' = "True"; 'Categories' = "Various"; 'Kind' = "Various"} $analysis += $object } $analysis } # Function used to build numbers in selection tables for menus function Add-IndexNumberToArray ( [Parameter(Mandatory=$True)] [array]$array ) { for($i=0; $i -lt $array.Count; $i++) { Add-Member -InputObject $array[$i] -Name "#" -Value ($i+1) -MemberType NoteProperty } $array } #Build the Log Array for each Resource Type function New-LogArray ( [Parameter(Mandatory=$True)] [array]$logCategories, [Parameter(Mandatory=$False)] $Dedicated ) { $logsArray += ' "logs": [' foreach ($element in $logCategories) { $logsArray += " { `"category`": `"$element`", `"enabled`": `"[parameters('logsEnabled')]`" }," } $logsArray = $logsArray.Substring(0,$logsArray.Length-1) $logsArray += ' ]' if($Dedicated) { $logsArray += ', "logAnalyticsDestinationType": "Dedicated"' } $logsArray } # Build the metric array (if relevant for the resourceType and export) function New-MetricArray { $metricsArray = ' "metrics": [ { "category": "AllMetrics", "enabled": "[parameters(''metricsEnabled'')]", "retentionPolicy": { "enabled": false, "days": 0 } } ]' $metricsArray } function Update-LogAnalyticsJSON ( [Parameter(Mandatory=$True)] [string]$resourceType, [Parameter(Mandatory=$False)] [string]$metricsArray, [Parameter(Mandatory=$False)] [string]$logsArray, [Parameter(Mandatory=$True)] [string]$nameField, [Parameter(Mandatory=$False)] [string]$ExportInitiative, [Parameter(Mandatory=$False)] [string]$JSONType, [Parameter(Mandatory=$False)] [string]$Kind, [Parameter(Mandatory=$False)] [string]$PolicyResourceDisplayName, [Parameter(Mandatory=$False)] [string]$PolicyName ) { if($Kind) { $JSONKind = @' , { "field": "kind", "equals": "<RESOURCE KIND>" } '@ } else { $JSONKind = $Null } $JSONARRAY=@() if($AllRegions) { $JSONPARMS = @' { "profileName": { "type": "String", "metadata": { "displayName": "Profile Name for Config", "description": "The profile name Azure Diagnostics" } }, "logAnalytics": { "type": "string", "metadata": { "displayName": "logAnalytics", "description": "The target Log Analytics Workspace for Azure Diagnostics", "strongType": "omsWorkspace" } }, "metricsEnabled": { "type": "String", "metadata": { "displayName": "Enable Metrics", "description": "Enable Metrics - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "False" }, "logsEnabled": { "type": "String", "metadata": { "displayName": "Enable Logs", "description": "Enable Logs - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "True" } } '@ $JSONRULES = @' { "if": { "allOf": [ { "field": "type", "equals": "<RESOURCE TYPE>" }<RESOURCE KIND> ] }, "then": { "effect": "deployIfNotExists", "details": { "type": "Microsoft.Insights/diagnosticSettings", "existenceCondition": { "allOf": [ { "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", "equals": "[parameters('LogsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", "equals": "[parameters('MetricsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/workspaceId", "equals": "[parameters('logAnalytics')]" } ] }, "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293" ], "deployment": { "properties": { "mode": "incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "name": { "type": "string" }, "logAnalytics": { "type": "string" }, "metricsEnabled": { "type": "string" }, "logsEnabled": { "type": "string" }, "profileName": { "type": "string" } }, "variables": {}, "resources": [ { "type": "<RESOURCE TYPE>/providers/diagnosticSettings", "apiVersion": "2017-05-01-preview", "name": "[concat(parameters('name'), '/', 'Microsoft.Insights/', parameters('profileName'))]", "properties": { "workspaceId": "[parameters('logAnalytics')]",<METRICS ARRAY><LOGS ARRAY> } } ], "outputs": { "policy": { "type": "string", "value": "[concat(parameters('logAnalytics'), 'configured for diagnostic logs for ', ': ', parameters('name'))]" } } }, "parameters": { "logAnalytics": { "value": "[parameters('logAnalytics')]" }, "name": { "value": "[field('<NAME OR FULLNAME>')]" }, "metricsEnabled": { "value": "[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[parameters('logsEnabled')]" }, "profileName": { "value": "[parameters('profileName')]" } } } } } } } '@ } else { $JSONPARMS = @' { "profileName": { "type": "String", "metadata": { "displayName": "Profile Name for Config", "description": "The profile name Azure Diagnostics" } }, "logAnalytics": { "type": "string", "metadata": { "displayName": "logAnalytics", "description": "The target Log Analytics Workspace for Azure Diagnostics", "strongType": "omsWorkspace" } }, "azureRegions": { "type": "Array", "metadata": { "displayName": "Allowed Locations", "description": "The list of locations that can be specified when deploying resources", "strongType": "location" } }, "metricsEnabled": { "type": "String", "metadata": { "displayName": "Enable Metrics", "description": "Enable Metrics - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "False" }, "logsEnabled": { "type": "String", "metadata": { "displayName": "Enable Logs", "description": "Enable Logs - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "True" } } '@ $JSONRULES = @' { "if": { "allOf": [ { "field": "type", "equals": "<RESOURCE TYPE>" }, { "field": "location", "in": "[parameters('AzureRegions')]" }<RESOURCE KIND> ] }, "then": { "effect": "deployIfNotExists", "details": { "type": "Microsoft.Insights/diagnosticSettings", "existenceCondition": { "allOf": [ { "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", "equals": "[parameters('LogsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", "equals": "[parameters('MetricsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/workspaceId", "equals": "[parameters('logAnalytics')]" } ] }, "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293" ], "deployment": { "properties": { "mode": "incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "name": { "type": "string" }, "location": { "type": "string" }, "logAnalytics": { "type": "string" }, "metricsEnabled": { "type": "string" }, "logsEnabled": { "type": "string" }, "profileName": { "type": "string" } }, "variables": {}, "resources": [ { "type": "<RESOURCE TYPE>/providers/diagnosticSettings", "apiVersion": "2017-05-01-preview", "name": "[concat(parameters('name'), '/', 'Microsoft.Insights/', parameters('profileName'))]", "location": "[parameters('location')]", "properties": { "workspaceId": "[parameters('logAnalytics')]",<METRICS ARRAY><LOGS ARRAY> } } ], "outputs": { "policy": { "type": "string", "value": "[concat(parameters('logAnalytics'), 'configured for diagnostic logs for ', ': ', parameters('name'))]" } } }, "parameters": { "logAnalytics": { "value": "[parameters('logAnalytics')]" }, "location": { "value": "[field('location')]" }, "name": { "value": "[field('<NAME OR FULLNAME>')]" }, "metricsEnabled": { "value": "[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[parameters('logsEnabled')]" }, "profileName": { "value": "[parameters('profileName')]" } } } } } } } '@ } if(!($ExportInitiative)) { $JSONVar = @' { '@ } $JSONVar = $JSONVar + $JSONType + @' "properties": { "displayName": "<POLICY RESOURCE DISPLAY NAME>", "mode": "Indexed", "description": "This policy automatically deploys diagnostic settings to <POLICYDISPLAYNAME>.", "metadata": { "category": "Monitoring" }, "parameters": <INSERT Parameters>, "policyRule": <INSERT Policy Rules> } } '@ # Update Content according to type, categories, fullname or name $JSONVar = $JSONVar.replace('<POLICY RESOURCE DISPLAY NAME>', $PolicyResourceDisplayName) $JSONVar = $JSONVar.replace('<POLICYDISPLAYNAME>', $PolicyResourceDisplayName) # Output content for Azure Rules file $JSONRules = $JSONRules.replace('<RESOURCE TYPE>', $ResourceType) $JSONRules = $JSONRules.replace('<METRICS ARRAY>', $metricsArray) $JSONRules = $JSONRules.replace('<LOGS ARRAY>', $logsArray) $JSONRules = $JSONRules.replace('<NAME OR FULLNAME>', $nameField) $JSONRULES = $JSONRules.Replace('<RESOURCE KIND>', $JSONKind) $JSONRULES = $JSONRules.Replace('<RESOURCE KIND>', $Kind) # Merge components to build azurepolicy output content $AzurePolicyJSON = $JSONVar $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Parameters>', $JSONPARMS) $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Policy Rules>',$JSONRULES) # Let's do some additional work if this is a policy initiative we are working on If($ExportInitiative) { # Retrieve parameters from Rules JSON for processing in initiative $RuleParams = $JSONRules | ConvertFrom-Json # Unescape non printable chars from string / JSON payload $RuleParams = $RuleParams.then.details.deployment.properties.parameters $RuleParams = $RuleParams |convertto-Json | ForEach-Object { [System.Text.RegularExpressions.Regex]::Unescape($_) } # Now add additional brackets to ensure ARM template doesn't get confused $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[field', ': "' + '[[field') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[last', ': "' + '[[last') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[parameters', ': "' + '[[parameters') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[name', ': "' + '[[name') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[concat', ': "' + '[[concat') # Clean up Init params variable $RuleParams = $RuleParams.Replace('"[field', '"[[field') $RuleParams = $RuleParams.Replace('"[last', '"[[last') $RuleParams = $RuleParams.Replace('"[parameters', '"[[parameters') $RuleParams = $RuleParams.Replace('"[name', '"[[name') $RuleParams = $RuleParams.Replace('"[concat', '"[[concat') } If(!($AllRegions)) { $locationParm = @' "azureRegions": { "value": "[[parameters('azureRegions')]" }, '@ } else { $locationParm = $Null } $initParams = @' "parameters": { "logAnalytics": { "value": "[[parameters('logAnalytics')]" },<LOCATION PARAM> "metricsEnabled": { "value": "[[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[[parameters('logsEnabled')]" }, "profileName": { "value": "[[parameters('profileName')]" } } '@ $initParams = $initParams.Replace("<LOCATION PARAM>", $locationParm) # Build the separate JSON payloads into an array $JSONARRAY += $JSONPARMS $JSONARRAY += $JSONRULES $JSONARRAY += $AzurePolicyJSON $JSONARRAY += $initParams # Send the payload $JSONARRAY } function Update-EventHubJSON ( [Parameter(Mandatory=$True)] [string]$resourceType, [Parameter(Mandatory=$False)] [string]$metricsArray, [Parameter(Mandatory=$False)] [string]$logsArray, [Parameter(Mandatory=$True)] [string]$nameField, [Parameter(Mandatory=$False)] [string]$ExportInitiative, [Parameter(Mandatory=$False)] [string]$JSONType, [Parameter(Mandatory=$False)] [string]$Kind, [Parameter(Mandatory=$False)] [string]$PolicyResourceDisplayName, [Parameter(Mandatory=$False)] [string]$PolicyName ) { if($Kind) { $JSONKind = @' , { "field": "kind", "equals": "<RESOURCE KIND>" } '@ } else { $JSONKind = $Null } $JSONARRAY=@() $JSONPARMS = @' { "profileName": { "type": "String", "metadata": { "displayName": "Profile Name for Config", "description": "The profile name Azure Diagnostics" } }, "eventHubName": { "type": "String", "metadata": { "displayName": "EventHub Name", "description": "The event hub for Azure Diagnostics", "strongType": "Microsoft.EventHub/Namespaces/EventHubs", "assignPermissions": true } }, "eventHubRuleId": { "type": "String", "metadata": { "displayName": "EventHubRuleID", "description": "The event hub RuleID for Azure Diagnostics", "strongType": "Microsoft.EventHub/Namespaces/AuthorizationRules", "assignPermissions": true } }, "azureRegions": { "type": "Array", "metadata": { "displayName": "Allowed Locations", "description": "The list of locations that can be specified when deploying resources", "strongType": "location" } }, "metricsEnabled": { "type": "String", "metadata": { "displayName": "Enable Metrics", "description": "Enable Metrics - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "False" }, "logsEnabled": { "type": "String", "metadata": { "displayName": "Enable Logs", "description": "Enable Logs - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "True" } } '@ If(!($AllRegions)) { $locationParm = @' "azureRegions": { "value": "[[parameters('azureRegions')]" }, '@ } else { $locationParm = $Null } $initParams = @' "parameters": { "eventHubName": { "value": "[[parameters('eventHubName')]" }, "eventHubRuleId": { "value": "[[parameters('eventHubRuleId')]" }, "azureRegions": { "value": "[[parameters('azureRegions')]" }, "metricsEnabled": { "value": "[[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[[parameters('logsEnabled')]" }, "profileName": { "value": "[[parameters('profileName')]" } } '@ $JSONRULES = @' { "if": { "allOf": [ { "field": "type", "equals": "<RESOURCE TYPE>" }, { "field": "location", "in": "[parameters('azureRegions')]" }<RESOURCE KIND> ] }, "then": { "effect": "deployIfNotExists", "details": { "type": "Microsoft.Insights/diagnosticSettings", "existenceCondition": { "allOf": [ { "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", "equals": "[parameters('logsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", "equals": "[parameters('metricsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/eventHubName", "equals": "[last(split(parameters('eventHubName'), '/'))]" } ] }, "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" ], "deployment": { "properties": { "mode": "incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "name": { "type": "string" }, "location": { "type": "string" }, "eventHubName": { "type": "string" }, "eventHubRuleId": { "type": "string" }, "metricsEnabled": { "type": "string" }, "logsEnabled": { "type": "string" }, "profileName": { "type": "string" } }, "variables": {}, "resources": [ { "type": "<RESOURCE TYPE>/providers/diagnosticSettings", "apiVersion": "2017-05-01-preview", "name": "[concat(parameters('name'), '/', 'Microsoft.Insights/', parameters('profileName'))]", "location": "[parameters('location')]", "properties": { "eventHubName": "[last(split(parameters('eventHubName'), '/'))]", "eventHubAuthorizationRuleId": "[parameters('eventHubRuleId')]",<METRICS ARRAY><LOGS ARRAY> } } ], "outputs": { "policy": { "type": "string", "value": "[concat(parameters('eventHubName'), 'configured for diagnostic logs for ', ': ', parameters('name'))]" } } }, "parameters": { "eventHubName": { "value": "[parameters('eventHubName')]" }, "location": { "value": "[field('location')]" }, "name": { "value": "[field('<NAME OR FULLNAME>')]" }, "eventHubRuleId": { "value": "[parameters('eventHubRuleId')]" }, "metricsEnabled": { "value": "[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[parameters('logsEnabled')]" }, "profileName": { "value": "[parameters('profileName')]" } } } } } } } '@ if(!($ExportInitiative)) { $JSONVar = @' { '@ } $JSONVar = $JSONVar + $JSONType + @' "properties": { "displayName": "<POLICY RESOURCE DISPLAY NAME>", "mode": "Indexed", "description": "This policy automatically deploys diagnostic settings to <POLICYDISPLAYNAME>.", "metadata": { "category": "Monitoring" }, "parameters": <INSERT Parameters>, "policyRule": <INSERT Policy Rules> } } '@ # Update Content according to type, categories, fullname or name $JSONVar = $JSONVar.replace('<POLICY RESOURCE DISPLAY NAME>', $PolicyResourceDisplayName) $JSONVar = $JSONVar.replace('<POLICYDISPLAYNAME>', $PolicyResourceDisplayName) # Output content for Azure Rules file $JSONRules = $JSONRules.replace('<RESOURCE TYPE>', $ResourceType) $JSONRULES = $JSONRULES.replace('<METRICS ARRAY>', $metricsArray) $JSONRULES = $JSONRULES.replace('<LOGS ARRAY>', $logsArray) $JSONRULES = $JSONRules.replace('<NAME OR FULLNAME>', $nameField) $JSONRULES = $JSONRULES.Replace('<RESOURCE KIND>', $JSONKind) $JSONRULES = $JSONRULES.Replace('<RESOURCE KIND>', $Kind) # Merge components to build azurepolicy output content $AzurePolicyJSON = $JSONVar $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Parameters>', $JSONPARMS) $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Policy Rules>',$JSONRULES) # Let's do some additional work if this is a policy initiative we are working on If($ExportInitiative) { # Retrieve parameters from Rules JSON for processing in initiative $RuleParams = $JSONRules | ConvertFrom-Json # Unescape non printable chars from string / JSON payload $RuleParams = $RuleParams.then.details.deployment.properties.parameters $RuleParams = $RuleParams |convertto-Json | ForEach-Object { [System.Text.RegularExpressions.Regex]::Unescape($_) } # Now add additional brackets to ensure ARM template doesn't get confused $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[field', ': "' + '[[field') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[last', ': "' + '[[last') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[parameters', ': "' + '[[parameters') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[name', ': "' + '[[name') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[concat', ': "' + '[[concat') # Clean up Init params variable $RuleParams = $RuleParams.Replace('"[field', '"[[field') $RuleParams = $RuleParams.Replace('"[last', '"[[last') $RuleParams = $RuleParams.Replace('"[parameters', '"[[parameters') $RuleParams = $RuleParams.Replace('"[name', '"[[name') $RuleParams = $RuleParams.Replace('"[concat', '"[[concat') } # Build the separate JSON payloads into an array $JSONARRAY += $JSONPARMS $JSONARRAY += $JSONRULES $JSONARRAY += $AzurePolicyJSON $JSONARRAY += $initParams # Send the payload $JSONARRAY } function Update-StorageJSON ( [Parameter(Mandatory=$True)] [string]$resourceType, [Parameter(Mandatory=$False)] [string]$metricsArray, [Parameter(Mandatory=$False)] [string]$logsArray, [Parameter(Mandatory=$True)] [string]$nameField, [Parameter(Mandatory=$False)] [string]$ExportInitiative, [Parameter(Mandatory=$False)] [string]$JSONType, [Parameter(Mandatory=$False)] [string]$Kind, [Parameter(Mandatory=$False)] [string]$PolicyResourceDisplayName, [Parameter(Mandatory=$False)] [string]$PolicyName ) { if($Kind) { $JSONKind = @' , { "field": "kind", "equals": "<RESOURCE KIND>" } '@ } else { $JSONKind = $Null } $JSONARRAY=@() $JSONPARMS = @' { "profileName": { "type": "String", "metadata": { "displayName": "Profile Name for Config", "description": "The profile name Azure Diagnostics" } }, "StorageAccountID": { "type": "String", "metadata": { "displayName": "StorageAccountID", "description": "The Storage Account ID for for Azure Diagnostics", "strongType": "Microsoft.Storage/StorageAccounts", "assignPermissions": true } }, "azureRegions": { "type": "Array", "metadata": { "displayName": "Allowed Locations", "description": "The list of locations that can be specified when deploying resources", "strongType": "location" } }, "metricsEnabled": { "type": "String", "metadata": { "displayName": "Enable Metrics", "description": "Enable Metrics - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "False" }, "logsEnabled": { "type": "String", "metadata": { "displayName": "Enable Logs", "description": "Enable Logs - True or False" }, "allowedValues": [ "True", "False" ], "defaultValue": "True" } } '@ If(!($AllRegions)) { $locationParm = @' "azureRegions": { "value": "[[parameters('azureRegions')]" }, '@ } else { $locationParm = $Null } $initParams = @' "parameters": { "storageAccountId": { "value": "[[parameters('storageAccountId')]" }, "azureRegions": { "value": "[[parameters('azureRegions')]" }, "metricsEnabled": { "value": "[[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[[parameters('logsEnabled')]" }, "profileName": { "value": "[[parameters('profileName')]" } } '@ $JSONRULES = @' { "if": { "allOf": [ { "field": "type", "equals": "<RESOURCE TYPE>" }, { "field": "location", "in": "[parameters('azureRegions')]" }<RESOURCE KIND> ] }, "then": { "effect": "deployIfNotExists", "details": { "type": "Microsoft.Insights/diagnosticSettings", "existenceCondition": { "allOf": [ { "field": "Microsoft.Insights/diagnosticSettings/logs.enabled", "equals": "[parameters('logsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/metrics.enabled", "equals": "[parameters('metricsEnabled')]" }, { "field": "Microsoft.Insights/diagnosticSettings/storageAccountId", "equals": "[last(split(parameters('storageAccountId'), '/'))]" } ] }, "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" ], "deployment": { "properties": { "mode": "incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "name": { "type": "string" }, "location": { "type": "string" }, "storageAccountId": { "type": "string" }, "metricsEnabled": { "type": "string" }, "logsEnabled": { "type": "string" }, "profileName": { "type": "string" } }, "variables": {}, "resources": [ { "type": "<RESOURCE TYPE>/providers/diagnosticSettings", "apiVersion": "2017-05-01-preview", "name": "[concat(parameters('name'), '/', 'Microsoft.Insights/', parameters('profileName'))]", "location": "[parameters('location')]", "properties": { "storageAccountId": "[parameters('storageAccountId')]",<METRICS ARRAY><LOGS ARRAY> } } ], "outputs": { "policy": { "type": "string", "value": "[concat(parameters('storageAccountId'), 'configured for diagnostic logs for ', ': ', parameters('name'))]" } } }, "parameters": { "location": { "value": "[field('location')]" }, "name": { "value": "[field('<NAME OR FULLNAME>')]" }, "storageAccountId": { "value": "[parameters('storageAccountId')]" }, "metricsEnabled": { "value": "[parameters('metricsEnabled')]" }, "logsEnabled": { "value": "[parameters('logsEnabled')]" }, "profileName": { "value": "[parameters('profileName')]" } } } } } } } '@ if(!($ExportInitiative)) { $JSONVar = @' { '@ } $JSONVar = $JSONVar + $JSONType + @' "properties": { "displayName": "<POLICY RESOURCE DISPLAY NAME>", "mode": "Indexed", "description": "This policy automatically deploys diagnostic settings to <POLICYDISPLAYNAME>.", "metadata": { "category": "Monitoring" }, "parameters": <INSERT Parameters>, "policyRule": <INSERT Policy Rules> } } '@ # Update Content according to type, categories, fullname or name $JSONVar = $JSONVar.replace('<POLICY RESOURCE DISPLAY NAME>', $PolicyResourceDisplayName) $JSONVar = $JSONVar.replace('<POLICYDISPLAYNAME>', $PolicyResourceDisplayName) # Output content for Azure Rules file $JSONRules = $JSONRules.replace('<RESOURCE TYPE>', $ResourceType) $JSONRULES = $JSONRULES.replace('<METRICS ARRAY>', $metricsArray) $JSONRULES = $JSONRULES.replace('<LOGS ARRAY>', $logsArray) $JSONRULES = $JSONRules.replace('<NAME OR FULLNAME>', $nameField) $JSONRULES = $JSONRULES.Replace('<RESOURCE KIND>', $JSONKind) $JSONRULES = $JSONRULES.Replace('<RESOURCE KIND>', $Kind) # Merge components to build azurepolicy output content $AzurePolicyJSON = $JSONVar $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Parameters>', $JSONPARMS) $AzurePolicyJSON = $AzurePolicyJSON.Replace('<INSERT Policy Rules>',$JSONRULES) # Let's do some additional work if this is a policy initiative we are working on If($ExportInitiative) { # Retrieve parameters from Rules JSON for processing in initiative $RuleParams = $JSONRules | ConvertFrom-Json # Unescape non printable chars from string / JSON payload $RuleParams = $RuleParams.then.details.deployment.properties.parameters $RuleParams = $RuleParams |convertto-Json | ForEach-Object { [System.Text.RegularExpressions.Regex]::Unescape($_) } # Now add additional brackets to ensure ARM template doesn't get confused $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[field', ': "' + '[[field') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[last', ': "' + '[[last') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[parameters', ': "' + '[[parameters') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[name', ': "' + '[[name') $AzurePolicyJSON = $AzurePolicyJSON.Replace(': "[concat', ': "' + '[[concat') # Clean up Init params variable $RuleParams = $RuleParams.Replace('"[field', '"[[field') $RuleParams = $RuleParams.Replace('"[last', '"[[last') $RuleParams = $RuleParams.Replace('"[parameters', '"[[parameters') $RuleParams = $RuleParams.Replace('"[name', '"[[name') $RuleParams = $RuleParams.Replace('"[concat', '"[[concat') } # Build the separate JSON payloads into an array $JSONARRAY += $JSONPARMS $JSONARRAY += $JSONRULES $JSONARRAY += $AzurePolicyJSON $JSONARRAY += $initParams # Send the payload $JSONARRAY } # Build File Name / paths for Azure Policy Exports function Parse-ResourceType ( [Parameter(Mandatory=$True)] [string]$resourceType, [Parameter(Mandatory=$True)] [string]$sinkDest, [Parameter(Mandatory=$False)] [string]$kind ) { $KindDirVar = $null if($Kind) { $pattern = '[^a-zA-Z0-9.-]' $KindDirVar = $Kind -replace $pattern, '-' $KindDirVar = "-" + $KindDirVar } $ReturnVar=@() if($ResourceType.Split("/").count -eq 3) { $nameField = "fullName" $DirectoryNameBase = "Apply-Diag-Settings-$sinkDest-" + $($resourceType.Split("/", 3))[0] + "-" + $($resourceType.Split("/", 3))[1] + "-" + $($resourceType.Split("/", 3))[2] + $KindDirVar } if($ResourceType.Split("/").count -eq 2) { $nameField = "name" $DirectoryNameBase = "Apply-Diag-Settings-$sinkDest-" + $($resourceType.Split("/", 2))[0] + "-" + $($resourceType.Split("/", 2))[1] + $KindDirVar } $ReturnVar += $DirectoryNameBase $ReturnVar += $nameField $ReturnVar } # Validate our JSON file is proper in syntax / format and can be leveraged Function Validate-JSON ( [Parameter(Mandatory=$True)] [string]$ExportDir ) { $filesToValidate = Get-ChildItem $ExportDir\*.json -rec $ValidCheck = @() foreach($File in $filesToValidate) { try { $null = Get-Content -Path $($File.pspath)|ConvertFrom-Json -ErrorAction stop; $ValidCheck += "VALID: " + "$($File.FullName)" } catch { $ValidCheck += "INVALID: " + "$($File.FullName)" } } $ValidCheck } function New-PolicyInitiative ( [Parameter(Mandatory=$True)] [string]$PolicyBag, [Parameter(Mandatory=$True)] [string]$PolicyRSIDs, [Parameter(Mandatory=$True)] [string]$PolicyDefParams, [Parameter(Mandatory=$True)] [string]$Parameters, [Parameter(Mandatory=$True)] [string]$sinkDest, [Parameter(Mandatory=$True)] [string]$InitiativeDisplayName, [Parameter(Mandatory=$True)] [string]$InitiativeName, [Parameter(Mandatory=$True)] [boolean]$ManagementGroupDeployment ) { # Scrub trailing commas $PolicyRSIDs = $PolicyRSIDs.substring(0,$PolicyRSIDs.length -3) $PolicyDefParams = $PolicyDefParams.substring(0,$PolicyDefParams.length -3) # Adding support for Management Group deployment scope. If parameter switch is used for -ManagementGroupDeployment, we'll put the right JSON in to support if($ManagementGroupDeployment -eq $true) { $MGJSONParam = @' { "TargetMGID": { "type": "string", "defaultValue": "" } } '@ $schema = "https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#" } else { $MGJSONParam = '{}' $schema = "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#" } # Build Template reference for Policy Initiative $InitiativeTemplate = @' { "$schema": "<SUB OR MG SCHEMA>", "contentVersion": "1.0.0.0", "parameters": <ManagementGroupID>, "resources": [ <AzurePolicyPropertyBag> { "type": "Microsoft.Authorization/policySetDefinitions", "apiVersion": "2020-09-01", "name": "<AZURE DIAG INITIATIVE NAME>", "dependsOn": [ <Policy INIT RESIDs> ], "properties": { "displayName": "<AZURE DIAG INITIATIVE DISPLAY NAME>", "description": "This initiative configures application Azure resources to forward diagnostic logs and metrics to an Azure Diagnostics sink point.", "metadata": { "category": "Monitoring" }, "parameters": <ParametersGoHere>, "policyDefinitions": [ <PolicyDefParams> ] } } ] } '@ # Update Policy Initiative reference strings according to what we've discovered $InitiativeTemplate = $InitiativeTemplate.Replace("<AZURE DIAG INITIATIVE NAME>", $InitiativeName) $InitiativeTemplate = $InitiativeTemplate.Replace("<AZURE DIAG INITIATIVE DISPLAY NAME>", $InitiativeDisplayName) $InitiativeTemplate = $InitiativeTemplate.Replace("<AzurePolicyPropertyBag>", $PolicyBag) $InitiativeTemplate = $InitiativeTemplate.Replace("<Policy INIT RESIDs>", $PolicyRSIDs) $InitiativeTemplate = $InitiativeTemplate.Replace("<ParametersGoHere>", $Parameters) $InitiativeTemplate = $InitiativeTemplate.Replace("<PolicyDefParams>", $PolicyDefParams) $InitiativeTemplate = $InitiativeTemplate.Replace("<ManagementGroupID>", $MGJSONParam) $InitiativeTemplate = $InitiativeTemplate.Replace("<SUB OR MG SCHEMA>", $schema) $InitiativeTemplate } # Prettify Function for JSON # Reference article / source: https://stackoverflow.com/questions/24789365/prettify-json-in-powershell-3/61988399#61988399 # Also credit to https://github.com/DeadPoolHeartsRR for their input on another script to use logic to use regex to clean up non printable chars function Format-JSON ($JSON) { $PrettifiedJSON = ($JSON) | convertfrom-json | convertto-json -depth 50 | ForEach-Object { [System.Text.RegularExpressions.Regex]::Unescape($_) } $PrettifiedJSON } # This function converts a string (name of a policy) into a hash equivelent for fitting into the max lenghth of policy object (64 chars) # http://xpertkb.com/compute-hash-string-powershell/ Function Create-Hash ($StringToHash) { $hasher = new-object System.Security.Cryptography.MD5CryptoServiceProvider $toHash = [System.Text.Encoding]::UTF8.GetBytes($StringToHash) $hashByteArray = $hasher.ComputeHash($toHash) foreach($byte in $hashByteArray) { $result += "{0:X2}" -f $byte } return $result; } # MAIN SCRIPT $Start = $(Get-date) If($LogsExport) { Write-host "You've opted to export policies for resources that only have logs supported .." -ForegroundColor Yellow } if (!($MyInvocation.MyCommand.Path)) { $CurrentDir = Split-Path $MyInvocation.MyCommand.Path } else { # Sometimes $myinvocation is null, it depends on the PS console host $CurrentDir = "." } Set-Location $CurrentDir # Determine where the script is running - build export dir IF(!$($ExportEH) -and !($ExportLA) -and !($ExportStorage) -and !($ValidateJSON)) { write-host "Nothing to do - please use either parameter -ExportLA, -ExportEH, -ExportStorage, -ValidateJSON (or all four) to export / validate policies" -ForegroundColor Yellow Set-Location $CurrentDir exit } # An initiative cannot support multiple sink points due to variance in parameters for each type of policy if($ExportInitiative -and (($($ExportEH.IsPresent) + $($ExportLA.IsPresent) + $($ExportStorage.IsPresent)) -gt 1)) { Write-host "Initiative Export option does not support more than one sink point for Policies together. Please choose parameter -ExportLA, -ExportStorage, or -ExportEH only when using -ExportInitiative" -ForegroundColor Yellow break } If(!($ExportDir)) { $ExportDir = $CurrentDir } #Variable Definitions [array]$Resources = @() $SubScriptionsToProcess = $null # Login to Azure - if already logged in, use existing credentials. If($ADO){write-host "ADO switch deprecated and no longer necessary" -ForegroundColor Yellow} Write-Host "Authenticating to Azure..." -ForegroundColor Cyan try { $AzureLogin = Get-AzSubscription $currentContext = Get-AzContext # Establish REST Token $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile $profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile) $token = $profileClient.AcquireAccessToken($currentContext.Subscription.TenantId) } catch { $null = Login-AzAccount -Environment $Environment $AzureLogin = Get-AzSubscription $currentContext = Get-AzContext # Establish REST Token $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile $profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile) $token = $profileClient.AcquireAccessToken($currentContext.Subscription.TenantId) } Try { $Subscription = Get-AzSubscription -SubscriptionId $subscriptionId } catch { Write-Host "Subscription not found" break } # Ensure this is the subscription where your Azure Resources are you want to send diagnostic data from If($AzureLogin -and !($SubscriptionID) -and !($Tenant) -and !($ManagementGroup)) { [array]$SubscriptionArray = Add-IndexNumberToArray (Get-AzSubscription) [int]$SelectedSub = 0 # use the current subscription if there is only one subscription available if ($SubscriptionArray.Count -eq 1) { $SelectedSub = 1 } # Get SubscriptionID if one isn't provided while($SelectedSub -gt $SubscriptionArray.Count -or $SelectedSub -lt 1) { Write-host "Please select a subscription from the list below" $SubscriptionArray | Select-Object "#", Id, Name | Format-Table try { $SelectedSub = Read-Host "Please enter a selection from 1 to $($SubscriptionArray.count)" } catch { Write-Warning -Message 'Invalid option, please try again.' } } if($($SubscriptionArray[$SelectedSub - 1].Name)) { $SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].Name) } elseif($($SubscriptionArray[$SelectedSub - 1].SubscriptionName)) { $SubscriptionName = $($SubscriptionArray[$SelectedSub - 1].SubscriptionName) } write-verbose "You Selected Azure Subscription: $SubscriptionName" if($($SubscriptionArray[$SelectedSub - 1].SubscriptionID)) { [guid]$SubscriptionID = $($SubscriptionArray[$SelectedSub - 1].SubscriptionID) } if($($SubscriptionArray[$SelectedSub - 1].ID)) { [guid]$SubscriptionID = $($SubscriptionArray[$SelectedSub - 1].ID) } } if($SubscriptionId -and !($Tenant) -and !($ManagementGroup)) { try{ $SubscriptionToUse = Select-AzSubscription -Subscription $SubscriptionId Write-Host "Selecting Azure Subscription: $($SubscriptionToUse.Subscription.Name) ..." -ForegroundColor Cyan } catch{ write-host "Something went wrong - please check subscriptionId and try again" -ForegroundColor Red break } } if($Tenant) { $SubScriptionsToProcess = Get-AzSubscription -TenantId $($token).TenantId } # If Management Group specified, but no ID provided, let's go get one to use If(!($ManagementGroupID) -and $ManagementGroup) { [array]$MgtGroupArray = Add-IndexNumberToArray (Get-AzManagementGroup) if(!$MgtGroupArray) { Write-host "Please make sure you have Management Groups that are accessible" exit 1 } [int]$SelectedMG = 0 # use the current Managment Group if there is only one MG available if ($MgtGroupArray.Count -eq 1) { $SelectedMG = 1 } # Get Management Group if one isn't provided while($SelectedMG -gt $MgtGroupArray.Count -or $SelectedMG -lt 1) { Write-host "Please select a Management Group from the list below" $MgtGroupArray | select-object "#", Name, DisplayName, Id | Format-Table try { write-host "If you don't see your ManagementGroupID try using the parameter -ManagementGroupID" -ForegroundColor Cyan $SelectedMG = Read-Host "Please enter a selection from 1 to $($MgtGroupArray.count)" } catch { Write-Warning -Message 'Invalid option, please try again.' } } if($($MgtGroupArray[$SelectedMG - 1].Name)) { $ManagementGroupID = $($MgtGroupArray[$SelectedMG - 1].Name) } write-verbose "You Selected Management Group: $ManagementGroupID" Write-Host "Selecting Management Group: $ManagementGroupID ..." -ForegroundColor Cyan } # If Management Group specified, let's validate the ID provided is correct (exists) if($ManagementGroup) { $SubScriptionsToProcess =@() if($ManagementGroupID) { # Determine cloud and ensure proper REST Endpoint defined $azEnvironment = Get-AzEnvironment -Name $Environment $GetBody = BuildBody -method "GET" $MGSubsDetailsURI = "$($azEnvironment.ResourceManagerUrl)/providers/microsoft.management/managementGroups/$($ManagementGroupID)/descendants?api-version=2018-03-01-preview" $GetResults = (Invoke-RestMethod -uri $MGSubsDetailsURI @GetBody).value foreach($Result in $GetResults| Where-Object {$_.type -eq "/subscriptions"}) { $SubScriptionsToProcess += $Result } } else { Write-Host "No ManagementGroupID found - ERROR!" } } # Determine which resourcetype to search on [array]$ResourcesToCheck = @() [array]$DiagnosticCapable=@() [array]$Logcategories = @() IF($($ExportEH) -or ($ExportLA) -or ($ExportStorage)) { # Build parameter set according to parameters provided. $FindResourceParams = @{} if($ResourceType) { $FindResourceParams['ResourceType'] = $ResourceType } if($ResourceGroupName) { $FindResourceParams['ResourceGroupName'] = $ResourceGroupName } if($ResourceName) { $FindResourceParams['Name'] = $ResourceName } if($SubscriptionId) { $ResourcesToCheck = Get-AzResource @FindResourceParams } # If resourceType defined, ensure it can support diagnostics configuration if($ResourceType) { try { $Resources = $ResourcesToCheck $DiagnosticCapable = Get-ResourceType -allResources $Resources [int]$ResourceTypeToProcess = 0 if ( $DiagnosticCapable.Count -eq 2) { $ResourceTypeToProcess = 1 } } catch { Throw write-host "No diagnostic capable resources of type $ResourceType available in selected subscription $SubscriptionID" -ForegroundColor Red } } # Gather a list of resources supporting Azure Diagnostic logs and metrics and display a table if(!($ResourceType)) { try { if($SubscriptionId -and !($Tenant)) { Write-Host "Gathering a list of monitorable Resource Types from Azure Subscription " -NoNewline -ForegroundColor Cyan Write-Host "$($SubscriptionToUse.Subscription.Name)..." -ForegroundColor Yellow # If we only want log policies - only export those otherwise export all If(!($LogPolicyOnly)) { $DiagnosticCapable = Add-IndexNumberToArray (Get-ResourceType -allResources $ResourcesToCheck).where({$_.metrics -eq $True -or $_.Logs -eq $True}) } else { $DiagnosticCapable = Add-IndexNumberToArray (Get-ResourceType -allResources $ResourcesToCheck).where({$_.Logs -eq $True}) } } elseif($Tenant -or ($ManagementGroup -and $ManagementGroupID)) { if($Tenant){Write-Host "Gathering a list of monitorable Resource Types from Azure AD Tenant " -ForegroundColor Cyan} if($ManagementGroup){Write-Host "Gathering a list of monitorable Resource Types from Management Group $($ManagementGroupID) " -ForegroundColor Cyan} Write-Host "A total of $($SubScriptionsToProcess.count) subscriptions to process..." foreach($Sub in $SubScriptionsToProcess) { if($Tenant) { $SubIDToProcess = $($Sub.SubscriptionId) $SubName = $($Sub.Name) } if($ManagementGroup) { $SubIDToProcess = $Sub.Name $SubName = $Sub.properties.displayName } $SelectedSub = Select-AzSubscription -SubscriptionID $SubIDToProcess Write-Host "Analyzing Subscription: $SubName" $ResourcesToCheck = Get-AzResource if($ResourcesToCheck) { if(!($DiagCapable)) { $DiagCapable = Get-ResourceType -allResources $ResourcesToCheck } else { $DiagCapable = Get-ResourceType -allResources $ResourcesToCheck -analysis $DiagCapable } } } # Add the "ALL" option after grabbing all resourceTypes from all subs in Tenant $object = New-Object -TypeName PSObject -Property @{'ResourceType' = "All"; 'Metrics' = "True"; 'Logs' = "True"; 'Categories' = "Various";'Kind' = "Various"} $DiagCapable += $object # If we only want log policies - only export those otherwise export all If(!($LogPolicyOnly)) { $DiagnosticCapable = Add-IndexNumberToArray ($DiagCapable).where({$_.metrics -eq $True -or $_.Logs -eq $True}) } else { $DiagnosticCapable = Add-IndexNumberToArray ($DiagCapable).where({$_.Logs -eq $True}) } } [int]$ResourceTypeToProcess = 0 if($($DiagnosticCapable|Where-Object {$_.ResourceType -ne "ALL"}).count -eq 1) { $ResourceTypeToProcess = 1 } while($ResourceTypeToProcess -gt $DiagnosticCapable.Count -or $ResourceTypeToProcess -lt 1 -and $ExportALL -ne $True) { Write-Host "The table below are the resource types that support sending diagnostics to Log Analytics and Event Hubs" $DiagnosticCapable | Select-Object "#", ResourceType, Metrics, Logs |Format-Table try { $ResourceTypeToProcess = Read-Host "Please select a number from 1 - $($DiagnosticCapable.count) to create custom policy (select resourceType ALL to create a policy for each RP)" } catch { Write-Warning -Message 'Invalid option, please try again.' } } $ResourceType = $DiagnosticCapable[$ResourceTypeToProcess -1].ResourceType } catch { if($SubscriptionId) { Throw write-host "No diagnostic capable resources available in selected subscription $SubscriptionID" -ForegroundColor Red } } } If($ResourceType -eq "ALL") { foreach($Type in $DiagnosticCapable|Where-Object {$_.ResourceType -ne "ALL"}) { # Initialize metrics and logs JSON content $metricsArray = '' $logsArray = '' if($Type.logs) { $Logcategories = $Type.Categories $logsArray = New-LogArray $Logcategories -Dedicated $Dedicated } if($Type.metrics) { $metricsArray = New-MetricArray if($type.Logs) { $metricsArray += "," } } if($ExportLA) { $sinkDest = "LA" $RPVar = Parse-ResourceType -resourceType $Type.ResourceType -sinkDest $sinkDest -kind $Type.Kind # If we have a kind for the resourceType let's add that to the policy evaluation rules (and add it to the displayname) if($Type.Kind) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) `($($Type.Kind)`) to a Log Analytics Workspace" } elseif (!($Type.Kind)) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) to a Log Analytics Workspace" } # Create a Policy Name that is 64 chars (or less) using hash as an option [unique and repeatable] $ShortNameRT = Create-Hash -StringToHash $PolicyResourceDisplayName if($ExportInitiative) { $JSONType = @' { "type": "Microsoft.Authorization/policyDefinitions", "apiVersion": "2020-09-01", "name": "<SHORT NAME OF SERVICE>", '@ # If we are exporting for Management Group - update RSID to support management group navigation if($ManagementGroupDeployment) { $PolicyRSID = """[concat('/providers/Microsoft.Management/managementGroups/', parameters('TargetMGID'), '/providers/Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } # If not exporting for MG, leverage standard ResourceID else { $PolicyRSID = """[resourceId('Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } $PolicyRSIDs = $PolicyRSIDs + " " + $PolicyRSID + "," + "`r`n" $JSONTYPE = $JSONType.replace("<SHORT NAME OF SERVICE>", "$($ShortNameRT)") $PolicyJSON = Update-LogAnalyticsJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -JSONType $JSONType -ExportInitiative $ExportInitiative -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT $PolicyBag = $PolicyBag + $PolicyJSON[2] + ',' + "`r`n" $PolicyDefParam = @' { "policyDefinitionId": <PolicyResoureceID>, <Policy Definition Parameters> }, '@ $PolicyDefParam = $PolicyDefParam.Replace("<PolicyResoureceID>", "$($PolicyRSID)") $PolicyDefParam = $PolicyDefParam.Replace("<Policy Definition Parameters>", "$($PolicyJSON[3])") $PolicyDefParams = $PolicyDefParams + $PolicyDefParam + "`r`n" } else { $JSONType = '' $PolicyJSON = Update-LogAnalyticsJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT } if(!($ExportInitiative)) { write-host "Exporting Log Analytics Custom Azure Policy for resourceType: $($Type.ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } } if($ExportEH) { $sinkDest = "EH" $RPVar = Parse-ResourceType -resourceType $Type.ResourceType -sinkDest $sinkDest -kind $Type.Kind # If we have a kind for the resourceType let's add that to the policy evaluation rules (and add it to the displayname) if($Type.Kind) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) `($($Type.Kind)`) to a Regional Event Hub" } elseif (!($Type.Kind)) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) to a Regional Event Hub" } # Create a Policy Name that is 64 chars (or less) using hash as an option [unique and repeatable] $ShortNameRT = Create-Hash -StringToHash $PolicyResourceDisplayName if($ExportInitiative) { $JSONType = @' { "type": "Microsoft.Authorization/policyDefinitions", "apiVersion": "2020-09-01", "name": "<SHORT NAME OF SERVICE>", '@ # If we are exporting for Management Group - update RSID to support management group navigation if($ManagementGroupDeployment) { $PolicyRSID = """[concat('/providers/Microsoft.Management/managementGroups/', parameters('TargetMGID'), '/providers/Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } # If not exporting for MG, leverage standard ResourceID else { $PolicyRSID = """[resourceId('Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } $PolicyRSIDs = $PolicyRSIDs + " " + $PolicyRSID + "," + "`r`n" $JSONTYPE = $JSONType.replace("<SHORT NAME OF SERVICE>", "$($ShortNameRT)") $PolicyJSON = Update-EventHubJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -JSONType $JSONType -ExportInitiative $ExportInitiative -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT $PolicyBag = $PolicyBag + $PolicyJSON[2] + ',' + "`r`n" $PolicyDefParam = @' { "policyDefinitionId": <PolicyResoureceID>, <Policy Definition Parameters> }, '@ $PolicyDefParam = $PolicyDefParam.Replace("<PolicyResoureceID>", "$($PolicyRSID)") $PolicyDefParam = $PolicyDefParam.Replace("<Policy Definition Parameters>", "$($PolicyJSON[3])") $PolicyDefParams = $PolicyDefParams + $PolicyDefParam + "`r`n" } else { $JSONType = '' $PolicyJSON = Update-EventHubJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT } if(!($ExportInitiative)) { write-host "Exporting Event Hub Custom Azure Policy for resourceType: $($Type.ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } } # Export for Storage Sink if($ExportStorage) { $sinkDest = "Storage" $RPVar = Parse-ResourceType -resourceType $Type.ResourceType -sinkDest $sinkDest -kind $Type.Kind # If we have a kind for the resourceType let's add that to the policy evaluation rules (and add it to the displayname) if($Type.Kind) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) `($($Type.Kind)`) to a Regional Storage Account" } elseif (!($Type.Kind)) { $PolicyResourceDisplayName = "Apply Diagnostic Settings for $($Type.ResourceType) to a Regional Storage Account" } # Create a Policy Name that is 64 chars (or less) using hash as an option [unique and repeatable] $ShortNameRT = Create-Hash -StringToHash $PolicyResourceDisplayName if($ExportInitiative) { $JSONType = @' { "type": "Microsoft.Authorization/policyDefinitions", "apiVersion": "2020-09-01", "name": "<SHORT NAME OF SERVICE>", '@ # If we are exporting for Management Group - update RSID to support management group navigation if($ManagementGroupDeployment) { $PolicyRSID = """[concat('/providers/Microsoft.Management/managementGroups/', parameters('TargetMGID'), '/providers/Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } # If not exporting for MG, leverage standard ResourceID else { $PolicyRSID = """[resourceId('Microsoft.Authorization/policyDefinitions/', '$($ShortNameRT)')]""" } $PolicyRSIDs = $PolicyRSIDs + " " + $PolicyRSID + "," + "`r`n" $JSONTYPE = $JSONType.replace("<SHORT NAME OF SERVICE>", "$($ShortNameRT)") $PolicyJSON = Update-StorageJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -JSONType $JSONType -ExportInitiative $ExportInitiative -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT $PolicyBag = $PolicyBag + $PolicyJSON[2] + ',' + "`r`n" $PolicyDefParam = @' { "policyDefinitionId": <PolicyResoureceID>, <Policy Definition Parameters> }, '@ $PolicyDefParam = $PolicyDefParam.Replace("<PolicyResoureceID>", "$($PolicyRSID)") $PolicyDefParam = $PolicyDefParam.Replace("<Policy Definition Parameters>", "$($PolicyJSON[3])") $PolicyDefParams = $PolicyDefParams + $PolicyDefParam + "`r`n" } else { $JSONType = '' $PolicyJSON = Update-StorageJSON -resourceType $Type.ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -kind $Type.Kind -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT } if(!($ExportInitiative)) { write-host "Exporting Storage Custom Azure Policy for resourceType: $($Type.ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } } } } elseif (!($ExportInitiative)) { # Initialize metrics and logs JSON content $metricsArray = '' $logsArray = '' if($DiagnosticCapable[$ResourceTypeToProcess -1].Logs) { $logcategories = $DiagnosticCapable[$ResourceTypeToProcess -1].Categories $logsArray = New-LogArray $Logcategories -Dedicated $Dedicated } else { } if($DiagnosticCapable[$ResourceTypeToProcess -1].Metrics) { $metricsArray = New-MetricArray if($DiagnosticCapable[$ResourceTypeToProcess -1].Logs) { $metricsArray += "," } } else { } if($ExportLA) { $RPVar = Parse-ResourceType -resourceType $ResourceType -sinkDest "LA" -kind $Type.Kind $PolicyJSON = Update-LogAnalyticsJSON -resourceType $ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT write-host "Exporting Log Analytics Custom Azure Policy for resourceType: $($ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } if($ExportEH) { $RPVar = Parse-ResourceType -resourceType $ResourceType -sinkDest "EH" -kind $Type.Kind $PolicyJSON = Update-EventHubJSON -resourceType $ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT write-host "Exporting Event Hub Custom Azure Policy for resourceType: $($ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } if($ExportStorage) { $RPVar = Parse-ResourceType -resourceType $ResourceType -sinkDest "Storage" -kind $Type.Kind $PolicyJSON = Update-StorageJSON -resourceType $ResourceType -metricsArray $metricsArray -logsArray $logsArray -nameField $RPVar[1] -PolicyResourceDisplayName $PolicyResourceDisplayName -PolicyName $ShortNameRT write-host "Exporting Storage Custom Azure Policy for resourceType: $($ResourceType)" -ForegroundColor Yellow # Make sure export directory exists! if(!(Test-path "$($ExportDir)\$($RPVar[0])")) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)\$($RPVar[0])" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # Clean Up JSON $PolicyJSON[0] = Format-JSON -JSON $PolicyJSON[0] $PolicyJSON[1] = Format-JSON -JSON $PolicyJSON[1] $PolicyJSON[2] = Format-JSON -JSON $PolicyJSON[2] # outputting JSON for Azure Policy $PolicyJSON[0] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.parameters.json" -Force $PolicyJSON[1] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.rules.json" -Force $PolicyJSON[2] | Out-File "$($ExportDir)\$($RPVar[0])\azurepolicy.json" -Force } } if($ExportInitiative) { # If a template file was not specified for export, let's build one with what we know if(!($TemplateFileName)) { $TemplateFileName = 'ARM-Template-azurepolicyinit.json' } # Otherwise, ensure we are doing our best to make sure the file is properly named (adding JSON extension and stripping folders) else { $TemplateFileName = ($TemplateFileName -split "\\")[-1] if(!($TemplateFileName.contains(".json"))) { $TemplateFileName = $TemplateFileName + ".json" } } # Make sure export directory exists! if(!(Test-path $($ExportDir))) { try { $NULL = new-item -ItemType Directory -Path "$($ExportDir)" } catch { Write-Host "Failed to create output folder $ExportDir - exiting.." -ForegroundColor red exit } } # If a display name was specified for Initiative, use it otherwise, let's build one according to what we know If($InitiativeDisplayName) { $InitiativeName = Create-Hash -StringToHash $InitiativeDisplayName } else { if($sinkDest -eq "EH") { $SinkName = "Regional Event Hub" } if($sinkDest -eq "LA") { $SinkName = "Log Analytics Workspace" } if($sinkDest -eq "Storage") { $SinkName = "Regional Storage Account" } $InitiativeDisplayName = "Azure Diagnostics Policy Initiative for $SinkName" $InitiativeName = Create-Hash -StringToHash $InitiativeDisplayName } # Building the Policy Initiative (Note only one sink point per policy initiative [Log Analytics or EventHub]) $PolicyInititiative = New-PolicyInitiative -PolicyBag $PolicyBag -PolicyRSIDs $PolicyRSIDs -PolicyDefParams $PolicyDefParams -Parameters $PolicyJSON[0] -sinkDest $sinkDest -InitiativeDisplayName $InitiativeDisplayName -InitiativeName $InitiativeName -ManagementGroupDeployment $ManagementGroupDeployment # Ensure JSON is formatted on export $PolicyInititiative = Format-JSON -JSON $PolicyInititiative # Export Initiative try{ $PolicyInititiative | Out-File "$($ExportDir)\$TemplateFileName" -Force Write-Host "Successfully wrote ARM template Policy Initiative to $($ExportDir)\$TemplateFileName" -ForegroundColor Yellow } catch{} } } # Function to validate JSON is correct with proper syntax IF($ValidateJSON) { Write-Host "Now Validating JSON in each exported policy artifact..." -ForegroundColor Cyan $Results = Validate-Json -ExportDir $ExportDir $InvalidCnt = $($Results| Where-Object {$_ -match "INVALID:*"}).count Write-Host "Total Valid Files Checked:" $($Results | Where-Object {$_ -match "VALID:*"}).count -ForegroundColor Green If($InvalidCnt -gt 0) { write-host "Total InValid Files Found:" $($Results| Where-Object {$_ -match "INVALID:*"}).count -ForegroundColor Yellow write-host "`nPlease review the following files for errors" write-host $($Results| Where-Object {$_ -match "INVALID:*"}) -ForegroundColor Yellow } else { write-host "Total Invalid Files Found: 0" -ForegroundColor Yellow } } $Stop = (Get-Date) $Count = 0 try { While(($ContextSet -ne $currentSub) -or ($Count -ge 5)) { write-host "`nSetting Context back to initial subscription $CurrentSub" $SetContext = Set-AzContext -Subscription $CurrentSub $ContextSet = $SetContext.Subscription.Name $Count++ } } catch { Write-Host "Failed to set context back to intial subscription $CurrentSub. Please review!" } Write-Host "Complete`n" -ForegroundColor Green Write-Host "Script execution time: " -nonewline Write-Host "$($($Stop - $Start).minutes) minutes and $($($Stop - $Start).seconds) seconds.`n" -ForegroundColor Cyan |