From 19dae5009879b427cddf58b8e7083dae130f3a18 Mon Sep 17 00:00:00 2001 From: kaiaschulz <58422100+kaiaschulz@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:51:09 +0100 Subject: [PATCH 1/5] Changed regex-pattern + check if type is byte[] --- pwsh/AzGovVizParallel.ps1 | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 05c519de..154805ac 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -365,7 +365,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.4', + $ProductVersion = '6.3.5', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -12461,7 +12461,16 @@ function processStorageAccountAnalysis { } else { try { - $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { $staticWebsitesState = $true @@ -12494,7 +12503,16 @@ function processStorageAccountAnalysis { } if ($listContainersSuccess -eq $true) { - $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { From 51ff09659c5e0d665e1731d2f5b677a9479d9a80 Mon Sep 17 00:00:00 2001 From: kaiaschulz <58422100+kaiaschulz@users.noreply.github.com> Date: Fri, 15 Dec 2023 08:44:22 +0100 Subject: [PATCH 2/5] prep devAzGovVizParallel + updated readme&history md --- README.md | 15 +- history.md | 5 + pwsh/dev/devAzGovVizParallel.ps1 | 33202 ++++++++++++++++++++++++++++- 3 files changed, 33147 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 39a0d9a2..0d7a1be0 100644 --- a/README.md +++ b/README.md @@ -78,17 +78,10 @@ The [Azure Governance Visualizer Accelerator](https://github.com/Azure/Azure-Gov ## Release history -__Changes__ (2023-Nov-13 / 6.3.4 Minor) - -* introduce new parameter `-ARMLocation`. Define the Azure Resource Manager (ARM) location to use (default is to use westeurope; this is used to optimize the built-in Azure RBAC Role definitions tracking) -* hardening the automated AzAPICall PowerShell module installation by adding retry mechanism in case of failure (Azure DevOps/GitHub) -* tolerating more up to date AzAPICall version when executing outside of Azure DevOps/GitHub -* update ARM API-version for Resources. Using `2023-07-01` instead of `2021-04-01` -* update `/.azuredevops/pipelines/AzGovViz.variables.yml` - * add parameter `-ARMLocation` -* update README.md - * update [API reference](#api-reference) -* use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.84 +__Changes__ (2023-Dec-15 / 6.3.5 Minor) + +* Checking if the response of the storage account properties request is a byte array (type 'byte[]') and decode it to a string +* Different handling of BOM (Byte order mark) for XML returns on storage account properties request (since Powershell version 7.4.0) [Full release history](history.md) diff --git a/history.md b/history.md index 304e4185..d5a7d7e5 100644 --- a/history.md +++ b/history.md @@ -4,6 +4,11 @@ ### Azure Governance Visualizer version 6 +__Changes__ (2023-Dec-15 / 6.3.5 Minor) + +* Checking if the response of the storage account properties request is a byte array (type 'byte[]') and decode it to a string +* Different handling of BOM (Byte order mark) for XML returns on storage account properties request (since Powershell version 7.4.0) + __Changes__ (2023-Nov-13 / 6.3.4 Minor) * introduce new parameter `-ARMLocation`. Define the Azure Resource Manager (ARM) location to use (default is to use westeurope; this is used to optimize the built-in Azure RBAC Role definitions tracking) diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index 1dd6a324..2387c3b7 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -365,7 +365,7 @@ Param $Product = 'AzGovViz', [string] - $ProductVersion = '6.3.4', + $ProductVersion = '6.3.5', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -645,69 +645,33143 @@ if ($ManagementGroupId -match ' ') { } #region Functions -. ".\$($ScriptPath)\functions\validateLeastPrivilegeForUser.ps1" -. ".\$($ScriptPath)\functions\getPolicyRemediation.ps1" -. ".\$($ScriptPath)\functions\getPolicyHash.ps1" -. ".\$($ScriptPath)\functions\detectPolicyEffect.ps1" -. ".\$($ScriptPath)\functions\exportResourceLocks.ps1" -. ".\$($ScriptPath)\functions\processHierarchyMapOnlyCustomData.ps1" -. ".\$($ScriptPath)\functions\processPrivateEndpoints.ps1" -. ".\$($ScriptPath)\functions\processNetwork.ps1" -. ".\$($ScriptPath)\functions\processStorageAccountAnalysis.ps1" -. ".\$($ScriptPath)\functions\processALZPolicyVersionChecker.ps1" -. ".\$($ScriptPath)\functions\getPIMEligible.ps1" -. ".\$($ScriptPath)\functions\testGuid.ps1" -. ".\$($ScriptPath)\functions\apiCallTracking.ps1" -. ".\$($ScriptPath)\functions\addRowToTable.ps1" -. ".\$($ScriptPath)\functions\testPowerShellVersion.ps1" -. ".\$($ScriptPath)\functions\setOutput.ps1" -. ".\$($ScriptPath)\functions\setTranscript.ps1" -. ".\$($ScriptPath)\functions\verifyModules3rd.ps1" -. ".\$($ScriptPath)\functions\checkAzGovVizVersion.ps1" -. ".\$($ScriptPath)\functions\handleCloudEnvironment.ps1" -. ".\$($ScriptPath)\functions\addHtParameters.ps1" -. ".\$($ScriptPath)\functions\selectMg.ps1" -. ".\$($ScriptPath)\functions\validateAccess.ps1" -. ".\$($ScriptPath)\functions\getEntities.ps1" -. ".\$($ScriptPath)\functions\setBaseVariablesMG.ps1" -. ".\$($ScriptPath)\functions\getTenantDetails.ps1" -. ".\$($ScriptPath)\functions\getDefaultManagementGroup.ps1" -. ".\$($ScriptPath)\functions\runInfo.ps1" -. ".\$($ScriptPath)\functions\processHierarchyMapOnly.ps1" -. ".\$($ScriptPath)\functions\getSubscriptions.ps1" -. ".\$($ScriptPath)\functions\detailSubscriptions.ps1" -. ".\$($ScriptPath)\functions\getOrphanedResources.ps1" -. ".\$($ScriptPath)\functions\getMDfCSecureScoreMG.ps1" -. ".\$($ScriptPath)\functions\getConsumption.ps1" -. ".\$($ScriptPath)\functions\cacheBuiltIn.ps1" -. ".\$($ScriptPath)\functions\prepareData.ps1" -. ".\$($ScriptPath)\functions\getGroupmembers.ps1" -. ".\$($ScriptPath)\functions\processAADGroups.ps1" -. ".\$($ScriptPath)\functions\processApplications.ps1" -. ".\$($ScriptPath)\functions\processManagedIdentities.ps1" -. ".\$($ScriptPath)\functions\createTagList.ps1" -. ".\$($ScriptPath)\functions\getResourceDiagnosticsCapability.ps1" -. ".\$($ScriptPath)\functions\getFileNaming.ps1" -. ".\$($ScriptPath)\functions\resolveObjectIds.ps1" -. ".\$($ScriptPath)\functions\namingValidation.ps1" -. ".\$($ScriptPath)\functions\removeInvalidFileNameChars.ps1" -. ".\$($ScriptPath)\functions\addIndexNumberToArray.ps1" -. ".\$($ScriptPath)\functions\processDiagramMermaid.ps1" -. ".\$($ScriptPath)\functions\buildMD.ps1" -. ".\$($ScriptPath)\functions\buildTree.ps1" -. ".\$($ScriptPath)\functions\buildJSON.ps1" -. ".\$($ScriptPath)\functions\buildPolicyAllJSON.ps1" -. ".\$($ScriptPath)\functions\stats.ps1" -#Region dataCollectionFunctions -. ".\$($ScriptPath)\functions\dataCollection\dataCollectionFunctions.ps1" -. ".\$($ScriptPath)\functions\processDataCollection.ps1" -. ".\$($ScriptPath)\functions\exportBaseCSV.ps1" -. ".\$($ScriptPath)\functions\html\htmlFunctions.ps1" -. ".\$($ScriptPath)\functions\processTenantSummary.ps1" -. ".\$($ScriptPath)\functions\processDefinitionInsights.ps1" -. ".\$($ScriptPath)\functions\processScopeInsightsMgOrSub.ps1" -. ".\$($ScriptPath)\functions\showMemoryUsage.ps1" -#EndRegion dataCollectionFunctions +function addHtParameters { + Write-Host 'Add Azure Governance Visualizer htParameters' + if ($LargeTenant -eq $true) { + $script:NoScopeInsights = $true + $NoResourceProvidersAtAll = $true + $PolicyAtScopeOnly = $true + $RBACAtScopeOnly = $true + } + + if ($ManagementGroupsOnly) { + $script:NoSingleSubscriptionOutput = $true + } + + if ($HierarchyMapOnly) { + $NoJsonExport = $true + } + + $script:azAPICallConf['htParameters'] += [ordered]@{ + DoAzureConsumption = [bool]$DoAzureConsumption + DoNotIncludeResourceGroupsOnPolicy = [bool]$DoNotIncludeResourceGroupsOnPolicy + DoNotIncludeResourceGroupsAndResourcesOnRBAC = [bool]$DoNotIncludeResourceGroupsAndResourcesOnRBAC + DoNotShowRoleAssignmentsUserData = [bool]$DoNotShowRoleAssignmentsUserData + HierarchyMapOnly = [bool]$HierarchyMapOnly + LargeTenant = [bool]$LargeTenant + ManagementGroupsOnly = [bool]$ManagementGroupsOnly + NoJsonExport = [bool]$NoJsonExport + NoMDfCSecureScore = [bool]$NoMDfCSecureScore + NoResourceProvidersDetailed = [bool]$NoResourceProvidersDetailed + NoResourceProvidersAtAll = [bool]$NoResourceProvidersAtAll + NoPolicyComplianceStates = [bool]$NoPolicyComplianceStates + NoResources = [bool]$NoResources + ProductVersion = $ProductVersion + PolicyAtScopeOnly = [bool]$PolicyAtScopeOnly + RBACAtScopeOnly = [bool]$RBACAtScopeOnly + DoPSRule = [bool]$DoPSRule + PSRuleFailedOnly = [bool]$PSRuleFailedOnly + NoALZPolicyVersionChecker = [bool]$NoALZPolicyVersionChecker + NoStorageAccountAccessAnalysis = [bool]$NoStorageAccountAccessAnalysis + GitHubActionsOIDC = [bool]$GitHubActionsOIDC + NoNetwork = [bool]$NoNetwork + ThrottleLimit = $ThrottleLimit + } + Write-Host 'htParameters:' + $azAPICallConf['htParameters'] | Format-Table -AutoSize | Out-String + Write-Host 'Add Azure Governance Visualizer htParameters succeeded' -ForegroundColor Green +} +function addIndexNumberToArray ( + [Parameter(Mandatory = $True)] + [array]$array +) { + for ($i = 0; $i -lt ($array).count; $i++) { + Add-Member -InputObject $array[$i] -Name '#' -Value ($i + 1) -MemberType NoteProperty + } + return $array +} +function addRowToTable() { + Param ( + [string]$level = 0, + [string]$mgName = '', + [string]$mgId = '', + [string]$mgParentId = '', + [string]$mgParentName = '', + [string]$mgASCSecureScore = '', + [string]$Subscription = '', + [string]$SubscriptionId = '', + [string]$SubscriptionQuotaId = '', + [string]$SubscriptionState = '', + [string]$SubscriptionASCSecureScore = '', + [string]$SubscriptionTags = '', + [int]$SubscriptionTagsCount = 0, + [string]$Policy = '', + [string]$PolicyAvailability = '', + [string]$PolicyDescription = '', + [string]$PolicyVariant = '', + [string]$PolicyType = '', + $PolicyIsALZ = '', + [string]$PolicyCategory = '', + [string]$PolicyDefinitionIdGuid = '', + [string]$PolicyDefinitionId = '', + [string]$PolicyDefintionScope = '', + [string]$PolicyDefintionScopeMgSub = '', + [string]$PolicyDefintionScopeId = '', + [int]$PolicyDefinitionsScopedLimit = 0, + [int]$PolicyDefinitionsScopedCount = 0, + [int]$PolicySetDefinitionsScopedLimit = 0, + [int]$PolicySetDefinitionsScopedCount = 0, + [string]$PolicyDefinitionEffectDefault = '', + [string]$PolicyDefinitionEffectFixed = '', + [string]$PolicyAssignmentScope = '', + [string]$PolicyAssignmentScopeMgSubRg = '', + [string]$PolicyAssignmentScopeName = '', + $PolicyAssignmentNotScopes = '', + [string]$PolicyAssignmentId = '', + [string]$PolicyAssignmentName = '', + [string]$PolicyAssignmentDisplayName = '', + [string]$PolicyAssignmentDescription = '', + [string]$PolicyAssignmentEnforcementMode = '', + $PolicyAssignmentNonComplianceMessages = '', + [string]$PolicyAssignmentIdentity = '', + [int]$PolicyAssignmentLimit = 0, + [int]$PolicyAssignmentCount = 0, + [int]$PolicyAssignmentAtScopeCount = 0, + $PolicyAssignmentParameters, + $PolicyAssignmentParametersFormated, + [int]$PolicySetAssignmentLimit = 0, + [int]$PolicySetAssignmentCount = 0, + [int]$PolicySetAssignmentAtScopeCount = 0, + [int]$PolicyAndPolicySetAssignmentAtScopeCount = 0, + [string]$PolicyAssignmentAssignedBy = '', + [string]$PolicyAssignmentCreatedBy = '', + [string]$PolicyAssignmentCreatedOn = '', + [string]$PolicyAssignmentUpdatedBy = '', + [string]$PolicyAssignmentUpdatedOn = '', + [string]$RoleDefinitionId = '', + [string]$RoleDefinitionName = '', + [string]$RoleAssignmentIdentityDisplayname = '', + [string]$RoleAssignmentIdentitySignInName = '', + [string]$RoleAssignmentIdentityObjectId = '', + [string]$RoleAssignmentIdentityObjectType = '', + [string]$RoleAssignmentId = '', + [string]$RoleAssignmentScope = '', + [string]$RoleAssignmentScopeName = '', + [string]$RoleAssignmentScopeRG = '', + [string]$RoleAssignmentScopeRes = '', + [string]$RoleAssignmentScopeType = '', + [string]$RoleAssignmentCreatedBy = '', + [string]$RoleAssignmentCreatedOn = '', + $RoleAssignmentCreatedOnUnformatted, + [string]$RoleAssignmentUpdatedBy = '', + [string]$RoleAssignmentUpdatedOn = '', + [string]$RoleIsCustom = '', + [string]$RoleAssignableScopes = '', + [int]$RoleAssignmentsLimit = 0, + [int]$RoleAssignmentsCount = 0, + [string]$RoleActions = '', + [string]$RoleNotActions = '', + [string]$RoleDataActions = '', + [string]$RoleNotDataActions = '', + $RoleCanDoRoleAssignments, + [int]$RoleSecurityCustomRoleOwner = 0, + [int]$RoleSecurityOwnerAssignmentSP = 0, + [string]$BlueprintName = '', + [string]$BlueprintId = '', + [string]$BlueprintDisplayName = '', + [string]$BlueprintDescription = '', + [string]$BlueprintScoped = '', + [string]$BlueprintAssignmentVersion = '', + [string]$BlueprintAssignmentId = '', + [string]$RoleAssignmentPIM = '', + [string]$RoleAssignmentPIMAssignmentType = '', + $RoleAssignmentPIMSlotStart = '', + $RoleAssignmentPIMSlotEnd = '' + ) + + $null = $script:newTable.Add([PSCustomObject]@{ + level = $level + mgName = $mgName + mgId = $mgId + mgParentId = $mgParentId + mgParentName = $mgParentName + mgASCSecureScore = $mgASCSecureScore + Subscription = $Subscription + SubscriptionId = $SubscriptionId + SubscriptionQuotaId = $SubscriptionQuotaId + SubscriptionState = $SubscriptionState + SubscriptionASCSecureScore = $SubscriptionASCSecureScore + SubscriptionTags = $SubscriptionTags + SubscriptionTagsCount = $SubscriptionTagsCount + Policy = $Policy + PolicyAvailability = $PolicyAvailability + PolicyDescription = $PolicyDescription + PolicyVariant = $PolicyVariant + PolicyType = $PolicyType + PolicyIsALZ = $PolicyIsALZ + PolicyCategory = $PolicyCategory + PolicyDefinitionIdGuid = $PolicyDefinitionIdGuid + PolicyDefinitionId = $PolicyDefinitionId + PolicyDefintionScope = $PolicyDefintionScope + PolicyDefintionScopeMgSub = $PolicyDefintionScopeMgSub + PolicyDefintionScopeId = $PolicyDefintionScopeId + PolicyDefinitionsScopedLimit = $PolicyDefinitionsScopedLimit + PolicyDefinitionsScopedCount = $PolicyDefinitionsScopedCount + PolicySetDefinitionsScopedLimit = $PolicySetDefinitionsScopedLimit + PolicySetDefinitionsScopedCount = $PolicySetDefinitionsScopedCount + PolicyDefinitionEffectDefault = $PolicyDefinitionEffectDefault + PolicyDefinitionEffectFixed = $PolicyDefinitionEffectFixed + PolicyAssignmentScope = $PolicyAssignmentScope + PolicyAssignmentScopeMgSubRg = $PolicyAssignmentScopeMgSubRg + PolicyAssignmentScopeName = $PolicyAssignmentScopeName + PolicyAssignmentNotScopes = $PolicyAssignmentNotScopes + PolicyAssignmentId = $PolicyAssignmentId + PolicyAssignmentName = $PolicyAssignmentName + PolicyAssignmentDisplayName = $PolicyAssignmentDisplayName + PolicyAssignmentDescription = $PolicyAssignmentDescription + PolicyAssignmentEnforcementMode = $PolicyAssignmentEnforcementMode + PolicyAssignmentNonComplianceMessages = $PolicyAssignmentNonComplianceMessages + PolicyAssignmentIdentity = $PolicyAssignmentIdentity + PolicyAssignmentLimit = $PolicyAssignmentLimit + PolicyAssignmentCount = $PolicyAssignmentCount + PolicyAssignmentAtScopeCount = $PolicyAssignmentAtScopeCount + PolicyAssignmentParameters = $PolicyAssignmentParameters + PolicyAssignmentParametersFormated = $PolicyAssignmentParametersFormated + PolicySetAssignmentLimit = $PolicySetAssignmentLimit + PolicySetAssignmentCount = $PolicySetAssignmentCount + PolicySetAssignmentAtScopeCount = $PolicySetAssignmentAtScopeCount + PolicyAndPolicySetAssignmentAtScopeCount = $PolicyAndPolicySetAssignmentAtScopeCount + PolicyAssignmentAssignedBy = $PolicyAssignmentAssignedBy + PolicyAssignmentCreatedBy = $PolicyAssignmentCreatedBy + PolicyAssignmentCreatedOn = $PolicyAssignmentCreatedOn + PolicyAssignmentUpdatedBy = $PolicyAssignmentUpdatedBy + PolicyAssignmentUpdatedOn = $PolicyAssignmentUpdatedOn + RoleDefinitionId = $RoleDefinitionId + RoleDefinitionName = $RoleDefinitionName + RoleAssignmentIdentityDisplayname = $RoleAssignmentIdentityDisplayname + RoleAssignmentIdentitySignInName = $RoleAssignmentIdentitySignInName + RoleAssignmentIdentityObjectId = $RoleAssignmentIdentityObjectId + RoleAssignmentIdentityObjectType = $RoleAssignmentIdentityObjectType + RoleAssignmentId = $RoleAssignmentId + RoleAssignmentScope = $RoleAssignmentScope + RoleAssignmentScopeName = $RoleAssignmentScopeName + RoleAssignmentScopeRG = $RoleAssignmentScopeRG + RoleAssignmentScopeRes = $RoleAssignmentScopeRes + RoleAssignmentScopeType = $RoleAssignmentScopeType + RoleIsCustom = $RoleIsCustom + RoleAssignableScopes = $RoleAssignableScopes + RoleAssignmentCreatedBy = $RoleAssignmentCreatedBy + RoleAssignmentCreatedOn = $RoleAssignmentCreatedOn + RoleAssignmentCreatedOnUnformatted = $RoleAssignmentCreatedOnUnformatted + RoleAssignmentUpdatedBy = $RoleAssignmentUpdatedBy + RoleAssignmentUpdatedOn = $RoleAssignmentUpdatedOn + RoleAssignmentsLimit = $RoleAssignmentsLimit + RoleAssignmentsCount = $RoleAssignmentsCount + RoleActions = $RoleActions + RoleNotActions = $RoleNotActions + RoleDataActions = $RoleDataActions + RoleNotDataActions = $RoleNotDataActions + RoleCanDoRoleAssignments = $RoleCanDoRoleAssignments + RoleSecurityCustomRoleOwner = $RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $RoleSecurityOwnerAssignmentSP + BlueprintName = $BlueprintName + BlueprintId = $BlueprintId + BlueprintDisplayName = $BlueprintDisplayName + BlueprintDescription = $BlueprintDescription + BlueprintScoped = $BlueprintScoped + BlueprintAssignmentVersion = $BlueprintAssignmentVersion + BlueprintAssignmentId = $BlueprintAssignmentId + RoleAssignmentPIM = $RoleAssignmentPIM + RoleAssignmentPIMAssignmentType = $RoleAssignmentPIMAssignmentType + RoleAssignmentPIMSlotStart = $RoleAssignmentPIMSlotStart + RoleAssignmentPIMSlotEnd = $RoleAssignmentPIMSlotEnd + }) +} +function apiCallTracking { + [CmdletBinding()]Param( + [string]$stage, + [string]$spacing + ) + #APITracking + $APICallTrackingCount = ($azAPICallConf['arrayAPICallTracking']).Count + $APICallTrackingRetriesCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.TryCounter -gt 1 } )).Count + $APICallTrackingGroupedByTargetEndpoint = $azAPICallConf['arrayAPICallTracking'] | Group-Object -Property TargetEndpoint + $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($azAPICallConf['arrayAPICallTracking'].where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count + Write-Host "$($spacing)$($stage) API call stats:" + $duarationStats = ($azAPICallConf['arrayAPICallTracking'].Duration | Measure-Object -Average -Maximum -Minimum) + Write-Host "$($spacing) API calls total count: $APICallTrackingCount ($APICallTrackingRetriesCount retries; $APICallTrackingRestartDueToDuplicateNextlinkCounterCount nextLinkReset) | average: $($duarationStats.Average) sec, maximum: $($duarationStats.Maximum) sec, minimum: $($duarationStats.Minimum) sec" + foreach ($targetEndpoint in $APICallTrackingGroupedByTargetEndpoint | Sort-Object -Property Name) { + $APICallTrackingRetriesCount = ($targetEndpoint.Group.where({ $_.TryCounter -gt 1 } )).Count + $APICallTrackingRestartDueToDuplicateNextlinkCounterCount = ($targetEndpoint.Group.where({ $_.RestartDueToDuplicateNextlinkCounter -gt 0 } )).Count + $duarationStats = ($targetEndpoint.Group.Duration | Measure-Object -Average -Maximum -Minimum) + Write-Host "$($spacing) API calls endpoint '$($targetEndpoint.Name) ($($azAPICallConf['azAPIEndpointUrls'].($targetEndpoint.Name)))' count: $($targetEndpoint.Count) ($APICallTrackingRetriesCount retries; $APICallTrackingRestartDueToDuplicateNextlinkCounterCount nextLinkReset) | average: $($duarationStats.Average) sec, maximum: $($duarationStats.Maximum) sec, minimum: $($duarationStats.Minimum) sec" + } +} +function buildJSON { + #$fileTimestamp = Get-Date -Format "yyyyMM-dd HHmmss" + $startJSON = Get-Date + $startBuildHt = Get-Date + + Write-Host 'Create Hierarchy JSON' + Write-Host ' Create ht for JSON' + + $htJSON = [ordered]@{} + $htJSON.ManagementGroups = [ordered]@{} + + $MgIds = ($optimizedTableForPathQuery) | Select-Object -Property level, MgId, MgName, mgParentId, mgParentName | Sort-Object -Property level, MgId -Unique + $grpScopePolicyDefinitionsCustom = (($htCacheDefinitionsPolicy).values).where( { $_.Type -eq 'Custom' }) | Group-Object ScopeMgSub + $grpMgScopePolicyDefinitionsCustom = ($grpScopePolicyDefinitionsCustom.where( { $_.Name -eq 'Mg' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId) + $grpSubScopePolicyDefinitionsCustom = ($grpScopePolicyDefinitionsCustom.where( { $_.Name -eq 'Sub' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId) + + $grpScopePolicySetDefinitionsCustom = (($htCacheDefinitionsPolicySet).values).where( { $_.Type -eq 'Custom' }) | Group-Object ScopeMgSub + $grpMgScopePolicySetDefinitionsCustom = $grpScopePolicySetDefinitionsCustom.where( { $_.Name -eq 'Mg' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId + $grpSubScopePolicySetDefinitionsCustom = $grpScopePolicySetDefinitionsCustom.where( { $_.Name -eq 'Sub' }).Group | Sort-Object -Property PolicyDefinitionId | Group-Object ScopeId + + $grpScopePolicyAssignments = ($htCacheAssignmentsPolicy).values | Group-Object -Property AssignmentScopeMgSubRg + $grpMgScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'Mg' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId + $grpSubScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'Sub' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not $JsonExportExcludeResourceGroups) { + $grpRGScopePolicyAssignments = $grpScopePolicyAssignments.where( { $_.Name -eq 'RG' }).Group | Sort-Object @{Expression = { $_.Assignment.Id } } | Group-Object -Property AssignmentScopeId + $htSubRGPolicyAssignments = @{} + foreach ($rgpa in $grpRGScopePolicyAssignments) { + $subId = ($rgpa.Name).split('/')[0] + if (-not $htSubRGPolicyAssignments.($subId)) { + $htSubRGPolicyAssignments.($subId) = @{} + } + if (-not $htSubRGPolicyAssignments.($subId).PolicyAssignments) { + $htSubRGPolicyAssignments.($subId).PolicyAssignments = @() + } + $htSubRGPolicyAssignments.($subId).PolicyAssignments += $rgpa.group + } + } + } + + $grpScopeRoleAssignments = ($htCacheAssignmentsRole).values | Group-Object -Property AssignmentScopeTenMgSubRgRes + $grpTenantScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Tenant' }).Group | Group-Object -Property AssignmentScopeId + $grpMgScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Mg' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId + $grpSubScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Sub' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if (-not $JsonExportExcludeResourceGroups) { + $grpRGScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'RG' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId + $htSubRGRoleAssignments = @{} + foreach ($rgra in $grpRGScopeRoleAssignments) { + $subId = ($rgra.Name).split('/')[0] + if (-not $htSubRGRoleAssignments.($subId)) { + $htSubRGRoleAssignments.($subId) = @{} + } + if (-not $htSubRGRoleAssignments.($subId).RoleAssignments) { + $htSubRGRoleAssignments.($subId).RoleAssignments = @() + } + $htSubRGRoleAssignments.($subId).RoleAssignments += $rgra.group + } + + #res + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if (-not $JsonExportExcludeResources) { + $grpResScopeRoleAssignments = $grpScopeRoleAssignments.where( { $_.Name -eq 'Res' }).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } } | Group-Object -Property AssignmentScopeId + $htSubResRoleAssignments = @{} + foreach ($resra in $grpResScopeRoleAssignments.Group) { + $raSplit = ($resra.Assignment.RoleAssignmentId).split('/') + $splitSubId = $raSplit[2] + $splitRg = $raSplit[4] + if (-not $htSubResRoleAssignments.($splitSubId)) { + $htSubResRoleAssignments.($splitSubId) = @{} + } + if (-not $htSubResRoleAssignments.($splitSubId).($splitRg)) { + $htSubResRoleAssignments.($splitSubId).($splitRg) = @{} + + } + + $resourceName = $resra.AssignmentScopeId.split('/')[2] + if (-not $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)")) { + $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)") = @{} + + } + if (-not $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments) { + $htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments = [ordered]@{} + + } + ($htSubResRoleAssignments.($splitSubId).($splitRg).("$($resra.ResourceType)_$($resourceName)").RoleAssignments.($resra.Assignment.RoleAssignmentId)) = $resra.Assignment + } + } + } + } + + } + + $bluePrintsAssignmentsAtScope = ($htCacheAssignmentsBlueprint).keys | Sort-Object + $bluePrintDefinitions = ($htCacheDefinitionsBlueprint).Keys | Sort-Object + $subscriptions = ($optimizedTableForPathQuery.where( { -not [string]::IsNullOrEmpty($_.subscriptionId) })) | Select-Object mgId, Subscription* | Sort-Object -Property subscriptionId -Unique + foreach ($mg in $MgIds) { + + $htJSON.ManagementGroups.($mg.MgId) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).MgId = $mg.MgId + $htJSON.ManagementGroups.($mg.MgId).MgName = $mg.MgName + $htJSON.ManagementGroups.($mg.MgId).mgParentId = $mg.mgParentId + $htJSON.ManagementGroups.($mg.MgId).mgParentName = $mg.mgParentName + $htJSON.ManagementGroups.($mg.MgId).level = $mg.level + $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).RoleAssignments = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions = [ordered]@{} + + foreach ($PolDef in (($grpMgScopePolicyDefinitionsCustom).where( { $_.Name -eq $mg.MgId })).group) { + $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom.($PolDef.Id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).PolicyDefinitionsCustom.($PolDef.Id) = $PolDef.Json + } + + foreach ($PolSetDef in (($grpMgScopePolicySetDefinitionsCustom).where( { $_.Name -eq $mg.MgId })).group) { + $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom.($PolSetDef.Id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).PolicySetDefinitionsCustom.($PolSetDef.Id) = $PolSetDef.Json + } + + foreach ($PolAssignment in ($grpMgScopePolicyAssignments).where( { $_.Name -eq $mg.MgId }).group) { + $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments.($PolAssignment.Assignment.id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).PolicyAssignments.($PolAssignment.Assignment.id) = $PolAssignment.Assignment + } + + foreach ($RoleAssignment in ($grpMgScopeRoleAssignments).where( { $_.Name -eq $mg.MgId }).group) { + $htJSON.ManagementGroups.($mg.MgId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment + } + + foreach ($BlueprintDefinition in ($bluePrintDefinitions).where( { $_ -like "/providers/Microsoft.Management/managementGroups/$($mg.MgId)/*" })) { + $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions.($BlueprintDefinition) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).BlueprintDefinitions.($BlueprintDefinition) = $BlueprintDefinition + } + + if (($htDiagnosticSettingsMgSub).mg.($mg.MgId)) { + foreach ($entry in ($htDiagnosticSettingsMgSub).mg.($mg.MgId).keys | Sort-Object) { + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry) = [ordered]@{} + foreach ($diagset in ($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.keys | Sort-Object) { + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Name = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticSettingName) + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Type = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticTargetType) + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).TargetId = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticTargetId) + $htJSON.ManagementGroups.($mg.MgId).DiagnosticSettings.($entry).Settings = (($htDiagnosticSettingsMgSub).mg.($mg.MgId).$entry.$diagset.DiagnosticCategories) + } + } + } + + foreach ($subscription in $subscriptions) { + if ($subscription.MgId -eq $mg.MgId) { + + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionName = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionQuotaId = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionState = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionTags = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionName = $subscription.Subscription + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionQuotaId = $subscription.SubscriptionQuotaId + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionState = $subscription.SubscriptionState + if ($htSubscriptionTags.($subscription.SubscriptionId)) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).SubscriptionTags = $htSubscriptionTags.($subscription.SubscriptionId).getEnumerator() | Sort-Object Key -CaseSensitive + } + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings = [ordered]@{} + + foreach ($PolDef in (($grpSubScopePolicyDefinitionsCustom).where( { $_.Name -eq $subscription.subscriptionId })).group) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom.($PolDef.Id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyDefinitionsCustom.($PolDef.Id) = $PolDef.Json + } + + foreach ($PolSetDef in (($grpSubScopePolicySetDefinitionsCustom).where( { $_.Name -eq $subscription.subscriptionId })).group) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom.($PolSetDef.Id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicySetDefinitionsCustom.($PolSetDef.Id) = $PolSetDef.Json + } + + foreach ($PolAssignment in ($grpSubScopePolicyAssignments).where( { $_.Name -eq $subscription.subscriptionId }).group) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments.($PolAssignment.Assignment.id) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).PolicyAssignments.($PolAssignment.Assignment.id) = $PolAssignment.Assignment + } + + foreach ($RoleAssignment in ($grpSubScopeRoleAssignments).where( { $_.Name -eq $subscription.subscriptionId }).group) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).RoleAssignments.($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment + } + + foreach ($BlueprintDefinition in ($bluePrintDefinitions).where( { $_ -like "/subscriptions/$($subscription.subscriptionId)/*" })) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions.($BlueprintDefinition) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintDefinitions.($BlueprintDefinition) = $BlueprintDefinition + } + + foreach ($BlueprintsAssignment in ($blueprintsAssignmentsAtScope).where( { $_ -like "/subscriptions/$($subscription.subscriptionId)/*" })) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments.($BlueprintsAssignment) = [ordered]@{} + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).BlueprintAssignments.($BlueprintsAssignment) = $BlueprintsAssignment + } + + if (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId)) { + foreach ($entry in ($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).keys | Sort-Object) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry) = [ordered]@{} + foreach ($diagset in ($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.keys | Sort-Object) { + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Name = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticSettingName) + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Type = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticTargetType) + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).TargetId = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticTargetId) + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).DiagnosticSettings.($entry).Settings = (($htDiagnosticSettingsMgSub).sub.($subscription.subscriptionId).$entry.$diagset.DiagnosticCategories) + } + } + } + + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not $JsonExportExcludeResourceGroups) { + $htTemp = @{} + if (-not $htTemp.ResourceGroups) { + $htTemp.ResourceGroups = @{} + } + + if ($htSubRGPolicyAssignments.($subscription.subscriptionId)) { + foreach ($rgpa in $htSubRGPolicyAssignments.($subscription.subscriptionId).PolicyAssignments) { + $rgName = ($rgpa.AssignmentScopeId).split('/')[1] + if (-not $htTemp.ResourceGroups.($rgName)) { + $htTemp.ResourceGroups.($rgName) = [ordered]@{} + } + if (-not $htTemp.ResourceGroups.($rgName).PolicyAssignments) { + $htTemp.ResourceGroups.($rgName).PolicyAssignments = [ordered]@{} + } + $htTemp.ResourceGroups.($rgName).PolicyAssignments.($rgpa.Assignment.id) = $rgpa.Assignment + } + } + } + } + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if (-not $JsonExportExcludeResourceGroups) { + if (-not $htTemp) { + $htTemp = @{} + } + if (-not $htTemp.ResourceGroups) { + $htTemp.ResourceGroups = @{} + } + if ($htSubRGRoleAssignments.($subscription.subscriptionId)) { + foreach ($rgra in $htSubRGRoleAssignments.($subscription.subscriptionId).RoleAssignments) { + $rgName = ($rgra.AssignmentScopeId).split('/')[1] + if (-not $htTemp.ResourceGroups.($rgName)) { + $htTemp.ResourceGroups.($rgName) = [ordered]@{} + } + if (-not $htTemp.ResourceGroups.($rgName).RoleAssignments) { + $htTemp.ResourceGroups.($rgName).RoleAssignments = [ordered]@{} + } + $htTemp.ResourceGroups.($rgName).RoleAssignments.($rgra.Assignment.RoleAssignmentId) = $rgra.Assignment + } + } + # + if (-not $JsonExportExcludeResources) { + if (-not $htTemp.ResourceGroups) { + $htTemp.ResourceGroups = @{} + } + if ($htSubResRoleAssignments.($subscription.subscriptionId)) { + foreach ($rg in $htSubResRoleAssignments.($subscription.subscriptionId).keys) { + foreach ($res in $htSubResRoleAssignments.($subscription.subscriptionId).($rg).Keys | Sort-Object) { + $rgName = ($resra.AssignmentScopeId).split('/')[1] + if (-not $htTemp.ResourceGroups.($rg)) { + $htTemp.ResourceGroups.($rg) = [ordered]@{} + } + if (-not $htTemp.ResourceGroups.($rg).Resources) { + $htTemp.ResourceGroups.($rg).Resources = [ordered]@{} + } + if (-not $htTemp.ResourceGroups.($rg).Resources.($res)) { + $htTemp.ResourceGroups.($rg).Resources.($res) = [ordered]@{} + } + if (-not $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments) { + $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments = [ordered]@{} + } + $htTemp.ResourceGroups.($rg).Resources.($res).RoleAssignments = $htSubResRoleAssignments.($subscription.subscriptionId).($rg).($res).RoleAssignments + } + } + } + } + } + } + + if ($htTemp) { + $sortedHt = [ordered]@{} + foreach ($key in ($htTemp.ResourceGroups.keys | Sort-Object)) { + $sortedHt.($key) = $htTemp.ResourceGroups.($key) + } + $htJSON.ManagementGroups.($mg.MgId).Subscriptions.($subscription.subscriptionId).ResourceGroups = $sortedHt + $htTemp = $null + $sortedHt = $null + } + } + } + } + + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { + if ($ManagementGroupsOnly) { + $JSONPath = "JSON_ManagementGroupsOnly_$($ManagementGroupId)" + } + else { + $JSONPath = "JSON_$($ManagementGroupId)" + } + + if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)") { + Write-Host ' Cleaning old state (Pipeline only)' + Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)" + } + } + else { + if ($ManagementGroupsOnly) { + $JSONPath = "JSON_ManagementGroupsOnly_$($ManagementGroupId)_$($fileTimestamp)" + } + else { + $JSONPath = "JSON_$($ManagementGroupId)_$($fileTimestamp)" + } + Write-Host " Creating new state ($($JSONPath)) (local only))" + } + + $null = New-Item -Name $JSONPath -ItemType directory -Path $outputPath + + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { + "The directory '$($JSONPath)' will be rebuilt during the AzDO Pipeline run. __Do not save any files in this directory, files and folders will be deleted!__" | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)ReadMe_important.md" -Encoding utf8 + } + + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Definitions" -ItemType directory -Path $outputPath + + + + + $htJSON.RoleDefinitions = [ordered]@{} + $pathRoleDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)RoleDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitions)")) { + $null = New-Item -Name $pathRoleDefinitions -ItemType directory -Path $outputPath + $pathRoleDefinitionCustom = "$($pathRoleDefinitions)$($DirectorySeparatorChar)Custom" + $pathRoleDefinitionBuiltIn = "$($pathRoleDefinitions)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathRoleDefinitionCustom)" -ItemType directory -Path $outputPath + $null = New-Item -Name "$($pathRoleDefinitionBuiltIn)" -ItemType directory -Path $outputPath + } + + if (($htCacheDefinitionsRole).Keys.Count -gt 0) { + foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { ($htCacheDefinitionsRole).($_).IsCustom }) | Sort-Object) { + $htJSON.RoleDefinitions.($roleDefinition) = ($htCacheDefinitionsRole).($roleDefinition).Json.properties + $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionCustom)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + } + foreach ($roleDefinition in ($htCacheDefinitionsRole).Keys.where( { -not ($htCacheDefinitionsRole).($_).IsCustom })) { + $jsonConverted = ($htCacheDefinitionsRole).($roleDefinition).Json | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathRoleDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsRole).($roleDefinition).Name ) ($(($htCacheDefinitionsRole).($roleDefinition).Id)).json" -Encoding utf8 + } + } + + $pathPolicyDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitions)")) { + $null = New-Item -Name $pathPolicyDefinitions -ItemType directory -Path $outputPath + $pathPolicyDefinitionBuiltIn = "$($pathPolicyDefinitions)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicyDefinitionBuiltIn)" -ItemType directory -Path $outputPath + } + if (($htCacheDefinitionsPolicy).Keys.Count -gt 0) { + foreach ($policyDefinition in ($htCacheDefinitionsPolicy).Keys.where( { ($htCacheDefinitionsPolicy).($_).Type -eq 'BuiltIn' })) { + $jsonConverted = ($htCacheDefinitionsPolicy).($policyDefinition).Json.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicyDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicy).($policyDefinition).displayName) ($(($htCacheDefinitionsPolicy).($policyDefinition).Json.name)).json" -Encoding utf8 + } + } + + $pathPolicySetDefinitions = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitions)")) { + $null = New-Item -Name $pathPolicySetDefinitions -ItemType directory -Path $outputPath + $pathPolicySetDefinitionBuiltIn = "$($pathPolicySetDefinitions)$($DirectorySeparatorChar)BuiltIn" + $null = New-Item -Name "$($pathPolicySetDefinitionBuiltIn)" -ItemType directory -Path $outputPath + } + if (($htCacheDefinitionsPolicySet).Keys.Count -gt 0) { + foreach ($policySetDefinition in ($htCacheDefinitionsPolicySet).Keys.where( { ($htCacheDefinitionsPolicySet).($_).Type -eq 'BuiltIn' })) { + $jsonConverted = ($htCacheDefinitionsPolicySet).($policySetDefinition).Json.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($pathPolicySetDefinitionBuiltIn)$($DirectorySeparatorChar)$(removeInvalidFileNameChars ($htCacheDefinitionsPolicySet).($policySetDefinition).displayName) ($(($htCacheDefinitionsPolicySet).($policySetDefinition).Json.name)).json" -Encoding utf8 + } + } + + $endBuildHt = Get-Date + Write-Host " ht for JSON creation duration: $((New-TimeSpan -Start $startBuildHt -End $endBuildHt).TotalSeconds) seconds" + + $startBuildJSON = Get-Date + Write-Host ' Build JSON' + + + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Tenant" -ItemType directory -Path $outputPath + + $htTree = [ordered]@{} + $htTree.'Tenant' = [ordered] @{} + $htTree.Tenant.TenantId = $azAPICallConf['checkContext'].Tenant.Id + $htTree.Tenant.RoleAssignments = [ordered]@{} + foreach ($RoleAssignment in ($grpTenantScopeRoleAssignments).Group | Sort-Object @{Expression = { $_.Assignment.RoleAssignmentId } }) { + + $htTree.Tenant.RoleAssignments.$($RoleAssignment.Assignment.RoleAssignmentId) = [ordered]@{} + $htTree.Tenant.RoleAssignments.$($RoleAssignment.Assignment.RoleAssignmentId) = $RoleAssignment.Assignment + + if ($RoleAssignment.Assignment.PIM -eq 'true') { + $pim = 'PIM_' + } + else { + $pim = '' + } + $jsonConverted = ($RoleAssignment.Assignment | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Tenant$($DirectorySeparatorChar)ra_$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Tenant" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($RoleAssignment.Assignment.ObjectType)_$($pim)$($RoleAssignment.Assignment.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + } + + $htTree.'Tenant'.'ManagementGroups' = [ordered] @{} + $json = $htTree.'Tenant' + + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)Assignments")) { + $null = New-Item -Name "$($JSONPath)$($DirectorySeparatorChar)Assignments" -ItemType directory -Path $outputPath + } + + buildTree -mgId $ManagementGroupId -json $json -prnt "$($JSONPath)$($DirectorySeparatorChar)Tenant" + + $htTree.'Tenant'.'CustomRoleDefinitions' = $htJSON.RoleDefinitions + + Write-Host " Exporting Tenant JSON '$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)$($fileName).json'" + $htTree | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($JSONPath)$($DirectorySeparatorChar)$($fileName).json" -Encoding utf8 -Force + + $endBuildJSON = Get-Date + Write-Host " Building JSON duration: $((New-TimeSpan -Start $startBuildJSON -End $endBuildJSON).TotalSeconds) seconds" + + $endJSON = Get-Date + Write-Host "Creating Hierarchy JSON duration: $((New-TimeSpan -Start $startJSON -End $endJSON).TotalSeconds) seconds" +} +function buildMD { + Write-Host 'Building Markdown' + $startBuildMD = Get-Date + $script:arrayMgs = [System.Collections.ArrayList]@() + $script:arraySubs = [System.Collections.ArrayList]@() + $script:arraySubsOos = [System.Collections.ArrayList]@() + $markdown = $null + $script:markdownhierarchyMgs = $null + $script:markdownhierarchySubs = $null + $script:markdownTable = $null + + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { + if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { + $markdown += @" +# Azure Governance Visualizer - Management Group Hierarchy + +## HierarchyMap (Mermaid) + +::: mermaid + graph $($MermaidDirection.ToUpper());`n +"@ + } + if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { + $marks = '```' + $markdown += @" +# Azure Governance Visualizer - Management Group Hierarchy + +## HierarchyMap (Mermaid) + +$($marks)mermaid + graph $($MermaidDirection.ToUpper());`n +"@ + } + + } + else { + $markdown += @" +# Azure Governance Visualizer - Management Group Hierarchy + +$executionDateTimeInternationalReadable ($currentTimeZone) + +## HierarchyMap (Mermaid) + +::: mermaid + graph $($MermaidDirection.ToUpper());`n +"@ + } + + processDiagramMermaid + + $markdown += @" +$markdownhierarchyMgs +$markdownhierarchySubs + classDef mgr fill:#D9F0FF,stroke:#56595E,color:#000000,stroke-width:1px; + classDef subs fill:#EEEEEE,stroke:#56595E,color:#000000,stroke-width:1px; +"@ + + if (($arraySubsOos).count -gt 0) { + $markdown += @' + classDef subsoos fill:#FFCBC7,stroke:#56595E,color:#000000,stroke-width:1px; +'@ + } + + $markdown += @" + classDef mgrprnts fill:#FFFFFF,stroke:#56595E,color:#000000,stroke-width:1px; + class $(($arrayMgs | Sort-Object -Unique) -join ',') mgr; + class $(($arraySubs | Sort-Object -Unique) -join ',') subs; +"@ + + if (($arraySubsOos).count -gt 0) { + $markdown += @" + class $(($arraySubsOos | Sort-Object -Unique) -join ',') subsoos; +"@ + } + + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { + if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { + $markdown += @" +class $mermaidprnts mgrprnts; +::: + +"@ + } + if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { +` + $marks = '```' + $markdown += @" +class $mermaidprnts mgrprnts; +$marks + +"@ + } + } + else { + $markdown += @" +class $mermaidprnts mgrprnts; +::: + +"@ + } + + $markdown += @" +## Summary +`n +"@ + if (-not $HierarchyMapOnly) { + $markdown += @" +Total Management Groups: $totalMgCount (depth $mgDepth)\`n +"@ + + if (($arraySubsOos).count -gt 0) { + $markdown += @" +Total Subscriptions: $totalSubIncludedAndExcludedCount ($totalSubOutOfScopeCount out-of-scope)\`n +"@ + } + else { + $markdown += @" +Total Subscriptions: $totalSubIncludedAndExcludedCount\`n +"@ + } + + $markdown += @" +Total Custom Policy definitions: $tenantCustomPoliciesCount\ +Total Custom PolicySet definitions: $tenantCustompolicySetsCount\ +Total Policy assignments: $($totalPolicyAssignmentsCount)\ +Total Policy assignments ManagementGroups $($totalPolicyAssignmentsCountMg)\ +Total Policy assignments Subscriptions $($totalPolicyAssignmentsCountSub)\ +Total Policy assignments ResourceGroups: $($totalPolicyAssignmentsCountRg)\ +Total Custom Role definitions: $totalRoleDefinitionsCustomCount\ +Total Role assignments: $totalRoleAssignmentsCount\ +Total Role assignments (Tenant): $totalRoleAssignmentsCountTen\ +Total Role assignments (ManagementGroups): $totalRoleAssignmentsCountMG\ +Total Role assignments (Subscriptions): $totalRoleAssignmentsCountSub\ +Total Role assignments (ResourceGroups and Resources): $totalRoleAssignmentsResourceGroupsAndResourcesCount\ +Total Blueprint definitions: $totalBlueprintDefinitionsCount\ +Total Blueprint assignments: $totalBlueprintAssignmentsCount\ +Total Resources: $totalResourceCount\ +Total Resource Types: $totalResourceTypesCount +"@ + + } + if ($HierarchyMapOnly) { + $mgsDetails = ($optimizedTableForPathQueryMg | Select-Object Level, MgId -Unique) + $mgDepth = ($mgsDetails.Level | Measure-Object -Maximum).Maximum + $totalMgCount = ($mgsDetails).count + $totalSubCount = ($optimizedTableForPathQuerySub).count + + $markdown += @" +Total Management Groups: $totalMgCount (depth $mgDepth)\ +Total Subscriptions: $totalSubCount +"@ + + } + + $markdown += @" +`n +## Hierarchy Table + +| **MgLevel** | **MgName** | **MgId** | **MgParentName** | **MgParentId** | **SubName** | **SubId** | +|-------------|-------------|-------------|-------------|-------------|-------------|-------------| +$markdownTable +"@ + + $markdown | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).md" -Encoding utf8 -Force + $endBuildMD = Get-Date + Write-Host "Building Markdown total duration: $((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildMD -End $endBuildMD).TotalSeconds) seconds)" +} +function buildPolicyAllJSON { + Write-Host 'Creating PolicyAll JSON' + $startPolicyAllJSON = Get-Date + $htPolicyAndPolicySet = [ordered]@{} + $htPolicyAndPolicySet.Policy = [ordered]@{} + $htPolicyAndPolicySet.PolicySet = [ordered]@{} + $htPolicyAndPolicySet.PolicyAssignment = [ordered]@{} + foreach ($policy in ($tenantPoliciesDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicyDefinitionId)) { + $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()) = [ordered]@{ + PolicyType = $policy.Type + ScopeMGLevel = $policy.ScopeMGLevel + Scope = $policy.Scope + ScopeId = $policy.scopeId + PolicyDisplayName = $policy.PolicyDisplayName + PolicyDefinitionName = $policy.PolicyDefinitionName + PolicyDefinitionId = $policy.PolicyDefinitionId + PolicyEffect = $policy.PolicyEffect + PolicyCategory = $policy.PolicyCategory + UniqueAssignmentsCount = $policy.UniqueAssignmentsCount + UniqueAssignments = $policy.UniqueAssignments + UsedInPolicySetsCount = $policy.UsedInPolicySetsCount + UsedInPolicySets = $policy.UsedInPolicySet4JSON + CreatedOn = $policy.CreatedOn + CreatedBy = $policy.CreatedByJson + UpdatedOn = $policy.UpdatedOn + UpdatedBy = $policy.UpdatedByJson + JSON = $policy.Json + } + } + foreach ($policySet in ($tenantPolicySetsDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicySetDefinitionId)) { + $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()) = [ordered]@{ + PolicySetType = $policySet.Type + ScopeMGLevel = $policySet.ScopeMGLevel + Scope = $policySet.Scope + ScopeId = $policySet.scopeId + PolicySetDisplayName = $policySet.PolicySetDisplayName + PolicySetDefinitionName = $policySet.PolicySetDefinitionName + PolicySetDefinitionId = $policySet.PolicySetDefinitionId + PolicySetCategory = $policySet.PolicySetCategory + UniqueAssignmentsCount = $policySet.UniqueAssignmentsCount + UniqueAssignments = $policySet.UniqueAssignments + PoliciesUsedCount = $policySet.PoliciesUsedCount + PoliciesUsed = $policySet.PoliciesUsed4JSON + CreatedOn = $policySet.CreatedOn + CreatedBy = $policySet.CreatedByJson + UpdatedOn = $policySet.UpdatedOn + UpdatedBy = $policySet.UpdatedByJson + JSON = $policySet.Json + } + } + foreach ($key in $htCacheAssignmentsPolicy.keys | Sort-Object) { + $htPolicyAndPolicySet.PolicyAssignment.($key.ToLower()) = $htCacheAssignmentsPolicy.($key).Assignment + } + Write-Host " Exporting PolicyAll JSON '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json'" + $htPolicyAndPolicySet | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json" -Encoding utf8 -Force + + $endPolicyAllJSON = Get-Date + Write-Host "Creating PolicyAll JSON duration: $((New-TimeSpan -Start $startPolicyAllJSON -End $endPolicyAllJSON).TotalSeconds) seconds" +} +function buildTree($mgId, $prnt) { + $getMg = $htEntities.values.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.id -eq $mgId }) + $childrenManagementGroups = $htEntities.values.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.parentId -eq "/providers/Microsoft.Management/managementGroups/$($getMg.Id)" }) + $mgNameValid = removeInvalidFileNameChars $getMg.Id + $mgDisplayNameValid = removeInvalidFileNameChars $getMg.displayName + $prntx = "$($prnt)$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)")) { + $null = New-Item -Name $prntx -ItemType directory -Path $outputPath + } + + if (-not $json.'ManagementGroups') { + $json.'ManagementGroups' = [ordered]@{} + } + $json = $json.'ManagementGroups'.($getMg.Id) = [ordered]@{} + foreach ($mgCap in $htJSON.ManagementGroups.($getMg.Id).keys) { + $json.$mgCap = $htJSON.ManagementGroups.($getMg.Id).$mgCap + if ($mgCap -eq 'PolicyDefinitionsCustom') { + $mgCapShort = 'pd' + foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pdc) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + if ($mgCap -eq 'PolicySetDefinitionsCustom') { + $mgCapShort = 'psd' + foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($psdc) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + if ($mgCap -eq 'PolicyAssignments') { + $mgCapShort = 'pa' + foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($pa) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + #marker + if ($mgCap -eq 'RoleAssignments') { + $mgCapShort = 'ra' + foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($ra) + if ($hlp.PIM -eq 'true') { + $pim = 'PIM_' + } + else { + $pim = '' + } + $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($prntx)$($DirectorySeparatorChar)$($mgCapShort)_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($mgCap)$($DirectorySeparatorChar)Mg$($DirectorySeparatorChar)$($mgNameValid) ($($mgDisplayNameValid))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + } + } + + if ($mgCap -eq 'Subscriptions') { + foreach ($sub in $htJSON.ManagementGroups.($getMg.Id).($mgCap).Keys) { + $subNameValid = removeInvalidFileNameChars $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).SubscriptionName + $subFolderName = "$($prntx)$($DirectorySeparatorChar)$($subNameValid) ($($sub))" + $null = New-Item -Name $subFolderName -ItemType directory -Path $outputPath + foreach ($subCap in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).Keys) { + if ($subCap -eq 'PolicyDefinitionsCustom') { + $subCapShort = 'pd' + foreach ($pdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pdc) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicyDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + if ($subCap -eq 'PolicySetDefinitionsCustom') { + $subCapShort = 'psd' + foreach ($psdc in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($psdc) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp.properties | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Definitions$($DirectorySeparatorChar)PolicySetDefinitions$($DirectorySeparatorChar)Custom$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + if ($subCap -eq 'PolicyAssignments') { + $subCapShort = 'pa' + foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($pa) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($subCap)$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($(removeInvalidFileNameChars $hlp.name)).json" -Encoding utf8 + } + } + #marker + if ($subCap -eq 'RoleAssignments') { + $subCapShort = 'ra' + foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($ra) + if ($hlp.PIM -eq 'true') { + $pim = 'PIM_' + } + else { + $pim = '' + } + $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($subCapShort)_$($pim)$($hlp.ObjectType)_$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)$($subCap)$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + } + } + + #RG Pol + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not $JsonExportExcludeResourceGroups) { + if ($subCap -eq 'ResourceGroups') { + foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { + $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" + } + foreach ($pa in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).PolicyAssignments.($pa) + if ([string]::IsNullOrEmpty($hlp.properties.displayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = removeInvalidFileNameChars $hlp.properties.displayName + } + $jsonConverted = $hlp | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)pa_$($displayName) ($($hlp.name)).json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)PolicyAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($displayName) ($($hlp.name)).json" -Encoding utf8 + } + } + } + } + } + + #RG RoleAss + #marker + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if (-not $JsonExportExcludeResourceGroups) { + if ($subCap -eq 'ResourceGroups') { + foreach ($rg in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).Keys | Sort-Object) { + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)")) { + $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)" -ItemType directory -Path "$($outputPath)" + } + foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).RoleAssignments.($ra) + if ($hlp.PIM -eq 'true') { + $pim = 'PIM_' + } + else { + $pim = '' + } + $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)ra_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + } + #res + if (-not $JsonExportExcludeResources) { + + foreach ($res in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.keys) { + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)")) { + $null = New-Item -Name "$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" -ItemType directory -Path "$($outputPath)" + } + foreach ($ra in $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.keys) { + $hlp = $htJSON.ManagementGroups.($getMg.Id).($mgCap).($sub).($subCap).($rg).Resources.($res).RoleAssignments.($ra) + if ($hlp.PIM -eq 'true') { + $pim = 'PIM_' + } + else { + $pim = '' + } + $jsonConverted = ($hlp | Select-Object -ExcludeProperty PIM) | ConvertTo-Json -Depth 99 + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($subFolderName)$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)$($DirectorySeparatorChar)ra_$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + $path = "$($JSONPath)$($DirectorySeparatorChar)Assignments$($DirectorySeparatorChar)RoleAssignments$($DirectorySeparatorChar)Sub$($DirectorySeparatorChar)$($subNameValid) ($($sub))$($DirectorySeparatorChar)$($rg)$($DirectorySeparatorChar)$($res)" + if (-not (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)")) { + $null = New-Item -Name $path -ItemType directory -Path $outputPath + } + $jsonConverted | Set-Content -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($path)$($DirectorySeparatorChar)$($hlp.ObjectType)_$($pim)$($hlp.RoleAssignmentId -replace '.*/').json" -Encoding utf8 + } + } + } + } + } + } + } + } + } + } + } + + if ($childrenManagementGroups.Count -eq 0) { + $json.'ManagementGroups' = @{} + } + else { + foreach ($childMg in $childrenManagementGroups | Sort-Object -Property Id) { + buildTree -mgId $childMg.Id -json $json -prnt $prntx + } + } +} +function cacheBuiltIn { + $startDefinitionsCaching = Get-Date + Write-Host 'Caching built-in Policy and RBAC Role definitions' + + $arrayBuiltInCaching = @('PolicyDefinitions', 'PolicyDefinitionsStatic', 'PolicySetDefinitions', 'RoleDefinitions') + + $arrayBuiltInCaching | ForEach-Object -Parallel { + + $builtInCapability = $_ + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy + $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet + $htCacheDefinitionsRole = $using:htCacheDefinitionsRole + $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $ValidPolicyEffects = $using:ValidPolicyEffects + $htHashesBuiltInPolicy = $using:htHashesBuiltInPolicy + #vars + $ARMLocation = $using:ARMLocation + $ignoreARMLocation = $using:ignoreARMLocation + #functions + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + $function:getPolicyHash = $using:funcGetPolicyHash + + if ($builtInCapability -eq 'PolicyDefinitions') { + $currentTask = 'Caching built-in Policy definitions' + Write-Host " $currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'BuiltIn'" + $method = 'GET' + $requestPolicyDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + Write-Host " $($requestPolicyDefinitionAPI.Count) built-in Policy definitions returned" + $builtinPolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq 'BuiltIn' } ) + + foreach ($builtinPolicyDefinition in $builtinPolicyDefinitions) { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()) = @{} + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Id = ($builtinPolicyDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeMGLevel = '' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Scope = 'n/a' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeMgSub = 'n/a' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ScopeId = 'n/a' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).DisplayName = $builtinPolicyDefinition.Properties.displayname + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Name = $builtinPolicyDefinition.Name + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Description = $builtinPolicyDefinition.Properties.description + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Type = $builtinPolicyDefinition.Properties.policyType + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Category = $builtinPolicyDefinition.Properties.metadata.category + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Version = $builtinPolicyDefinition.Properties.metadata.version + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).PolicyDefinitionId = ($builtinPolicyDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($builtinPolicyDefinition.Properties.displayname)" + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZ = $false + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZState = '' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZLatestVer = '' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZIdentificationLevel = '' + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).ALZPolicyName = '' + if ($builtinPolicyDefinition.Properties.metadata.deprecated -eq $true -or $builtinPolicyDefinition.Properties.displayname -like "``[Deprecated``]*") { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Deprecated = $builtinPolicyDefinition.Properties.metadata.deprecated + } + else { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Deprecated = $false + } + if ($builtinPolicyDefinition.Properties.metadata.preview -eq $true -or $builtinPolicyDefinition.Properties.displayname -like "``[*Preview``]*") { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Preview = $builtinPolicyDefinition.Properties.metadata.preview + } + else { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Preview = $false + } + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $builtinPolicyDefinition + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue + #endregion effect + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Json = $builtinPolicyDefinition + + if (-not [string]::IsNullOrWhiteSpace($builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds + foreach ($roledefinitionId in $builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { + if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$builtinPolicyDefinition.Id + } + else { + $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies + $usedInPolicies += $builtinPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + } + } + } + else { + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = 'n/a' + } + + #hashes for parity builtin/custom + # $script:htHashesBuiltInPolicy.(($builtinPolicyDefinition.Id).ToLower()) = @{ + # policyRuleHash = getPolicyHash -object ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99) + # } + $policyRuleHash = (getPolicyHash -json ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99)) + if (-not $htHashesBuiltInPolicy.($policyRuleHash)) { + $script:htHashesBuiltInPolicy.($policyRuleHash) = @{ + Policies = [System.Collections.ArrayList]@() + } + $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) + } + else { + #Write-Host "$($builtinPolicyDefinition.name) $($policyRuleHash) already exists" + $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) + #$htHashesBuiltInPolicy.($policyRuleHash).Policies.Count + } + } + Write-Host " $($htHashesBuiltInPolicy.Keys.Count) unique Policy rule hashes for built-in Policy definitions" + } + + if ($builtInCapability -eq 'PolicyDefinitionsStatic') { + $currentTask = 'Caching static Policy definitions' + Write-Host " $currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Static'" + $method = 'GET' + $requestPolicyDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + Write-Host " $($requestPolicyDefinitionAPI.Count) static Policy definitions returned" + $staticPolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq 'Static' } ) + + foreach ($staticPolicyDefinition in $staticPolicyDefinitions) { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()) = @{} + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Id = ($staticPolicyDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeMGLevel = '' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Scope = 'n/a' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeMgSub = 'n/a' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ScopeId = 'n/a' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).DisplayName = $staticPolicyDefinition.Properties.displayname + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Name = $staticPolicyDefinition.Name + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Description = $staticPolicyDefinition.Properties.description + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Type = $staticPolicyDefinition.Properties.policyType + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Category = $staticPolicyDefinition.Properties.metadata.category + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Version = $staticPolicyDefinition.Properties.metadata.version + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).PolicyDefinitionId = ($staticPolicyDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($staticPolicyDefinition.Properties.displayname)" + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZ = $false + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZState = '' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZLatestVer = '' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZIdentificationLevel = '' + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).ALZPolicyName = '' + + if ($staticPolicyDefinition.Properties.metadata.deprecated -eq $true -or $staticPolicyDefinition.Properties.displayname -like "``[Deprecated``]*") { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Deprecated = $staticPolicyDefinition.Properties.metadata.deprecated + } + else { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Deprecated = $false + } + if ($staticPolicyDefinition.Properties.metadata.preview -eq $true -or $staticPolicyDefinition.Properties.displayname -like "``[*Preview``]*") { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Preview = $staticPolicyDefinition.Properties.metadata.preview + } + else { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Preview = $false + } + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $staticPolicyDefinition + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue + #endregion effect + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Json = $staticPolicyDefinition + + if (-not [string]::IsNullOrWhiteSpace($staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).RoleDefinitionIds = $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds + foreach ($roledefinitionId in $staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { + if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$staticPolicyDefinition.Id + } + else { + $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies + $usedInPolicies += $staticPolicyDefinition.Id + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + } + } + } + else { + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).RoleDefinitionIds = 'n/a' + } + } + } + + if ($builtInCapability -eq 'PolicySetDefinitions') { + + $currentTask = 'Caching built-in PolicySet definitions' + Write-Host " $currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/policySetDefinitions?api-version=2021-06-01&`$filter=policyType eq 'BuiltIn'" + $method = 'GET' + $requestPolicySetDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + $builtinPolicySetDefinitions = $requestPolicySetDefinitionAPI.where( { $_.properties.policyType -eq 'BuiltIn' } ) + Write-Host " $($requestPolicySetDefinitionAPI.Count) built-in PolicySet definitions returned" + foreach ($builtinPolicySetDefinition in $builtinPolicySetDefinitions) { + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()) = @{} + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Id = ($builtinPolicySetDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeMGLevel = '' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Scope = 'n/a' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeMgSub = 'n/a' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ScopeId = 'n/a' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).DisplayName = $builtinPolicySetDefinition.Properties.displayname + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Name = $builtinPolicySetDefinition.Name + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Description = $builtinPolicySetDefinition.Properties.description + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Type = $builtinPolicySetDefinition.Properties.policyType + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Category = $builtinPolicySetDefinition.Properties.metadata.category + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Version = $builtinPolicySetDefinition.Properties.metadata.version + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicyDefinitionId = ($builtinPolicySetDefinition.Id).ToLower() + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).LinkToAzAdvertizer = "$($builtinPolicySetDefinition.Properties.displayname)" + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZ = $false + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZState = '' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZLatestVer = '' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZIdentificationLevel = '' + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).ALZPolicySetName = '' + $arrayPolicySetPolicyIdsToLower = @() + $htPolicySetPolicyRefIds = @{} + $arrayPolicySetPolicyIdsToLower = foreach ($policySetPolicy in $builtinPolicySetDefinition.properties.policydefinitions) { + ($policySetPolicy.policyDefinitionId).ToLower() + $htPolicySetPolicyRefIds.($policySetPolicy.policyDefinitionReferenceId) = ($policySetPolicy.policyDefinitionId) + } + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicySetPolicyIds = $arrayPolicySetPolicyIdsToLower + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).PolicySetPolicyRefIds = $htPolicySetPolicyRefIds + if ($builtinPolicySetDefinition.Properties.metadata.deprecated -eq $true -or $builtinPolicySetDefinition.Properties.displayname -like "``[Deprecated``]*") { + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Deprecated = $builtinPolicySetDefinition.Properties.metadata.deprecated + } + else { + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Deprecated = $false + } + if ($builtinPolicySetDefinition.Properties.metadata.preview -eq $true -or $builtinPolicySetDefinition.Properties.displayname -like "``[*Preview``]*") { + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Preview = $builtinPolicySetDefinition.Properties.metadata.preview + } + else { + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Preview = $false + } + $script:htCacheDefinitionsPolicySet.(($builtinPolicySetDefinition.Id).ToLower()).Json = $builtinPolicySetDefinition + } + } + + if ($builtInCapability -eq 'RoleDefinitions') { + #Write-Host "`$ignoreARMLocation = '$ignoreARMLocation'" -ForegroundColor Yellow + if ($ignoreARMLocation) { + $currentTask = 'Caching built-in Role definitions' + Write-Host " $currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].'ARM')/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" + #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" + } + else { + $currentTask = "Caching built-in Role definitions (Location: '$($ARMLocation)')" + Write-Host " $currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls']."ARM$($ARMLocation)")/subscriptions/$($azAPICallConf['checkContext'].Subscription.Id)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" + #$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&`$filter=type eq 'BuiltInRole'" + } + + $method = 'GET' + $requestRoleDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + Write-Host " $($requestRoleDefinitionAPI.Count) built-in Role definitions returned" + foreach ($roleDefinition in $requestRoleDefinitionAPI) { + if ( + ( + $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/roleassignments/write' -or + $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/roleassignments/*' -or + $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/*/write' -or + $roleDefinition.properties.permissions.actions -contains 'Microsoft.Authorization/*' -or + $roleDefinition.properties.permissions.actions -contains '*/write' -or + $roleDefinition.properties.permissions.actions -contains '*' + ) -and ( + $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/roleassignments/write' -and + $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/roleassignments/*' -and + $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/*/write' -and + $roleDefinition.properties.permissions.notActions -notcontains 'Microsoft.Authorization/*' -and + $roleDefinition.properties.permissions.notActions -notcontains '*/write' -and + $roleDefinition.properties.permissions.notActions -notcontains '*' + ) + ) { + $roleCapable4RoleAssignmentsWrite = $true + } + else { + $roleCapable4RoleAssignmentsWrite = $false + } + + ($script:htCacheDefinitionsRole).($roleDefinition.name) = @{} + ($script:htCacheDefinitionsRole).($roleDefinition.name).Id = ($roleDefinition.name) + ($script:htCacheDefinitionsRole).($roleDefinition.name).Name = ($roleDefinition.properties.roleName) + ($script:htCacheDefinitionsRole).($roleDefinition.name).IsCustom = $false + ($script:htCacheDefinitionsRole).($roleDefinition.name).AssignableScopes = ($roleDefinition.properties.assignableScopes) + ($script:htCacheDefinitionsRole).($roleDefinition.name).Actions = ($roleDefinition.properties.permissions.actions) + ($script:htCacheDefinitionsRole).($roleDefinition.name).NotActions = ($roleDefinition.properties.permissions.notActions) + ($script:htCacheDefinitionsRole).($roleDefinition.name).DataActions = ($roleDefinition.properties.permissions.dataActions) + ($script:htCacheDefinitionsRole).($roleDefinition.name).NotDataActions = ($roleDefinition.properties.permissions.notDataActions) + ($script:htCacheDefinitionsRole).($roleDefinition.name).Json = ($roleDefinition.properties) + ($script:htCacheDefinitionsRole).($roleDefinition.name).LinkToAzAdvertizer = "$($roleDefinition.properties.roleName)" + ($script:htCacheDefinitionsRole).($roleDefinition.name).RoleCanDoRoleAssignments = $roleCapable4RoleAssignmentsWrite + } + } + } + + $script:builtInPolicyDefinitionsCount = $htCacheDefinitionsPolicy.Values.where({ $_.Type -eq 'BuiltIn' }).count + + $endDefinitionsCaching = Get-Date + Write-Host "Caching built-in definitions duration: $((New-TimeSpan -Start $startDefinitionsCaching -End $endDefinitionsCaching).TotalSeconds) seconds" +} +function checkAzGovVizVersion { + try { + $getRepoVersion = Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/master/version.json' + $repoVersion = ($getRepoVersion.Content | ConvertFrom-Json).ProductVersion + + $script:azGovVizNewerVersionAvailable = $false + if ($repoVersion -ne $ProductVersion) { + $repoVersionSplit = $repoVersion -split '\.' + $repoVersionMajor = $repoVersionSplit[0] + $repoVersionMinor = $repoVersionSplit[1] + $repoVersionPatch = $repoVersionSplit[2] + + $ProductVersionSplit = $ProductVersion -split '\.' + $ProductVersionMajor = $ProductVersionSplit[0] + $ProductVersionMinor = $ProductVersionSplit[1] + $ProductVersionPatch = $ProductVersionSplit[2] + + if ($repoVersionMajor -ne $ProductVersionMajor) { + $versionDrift = 'major' + } + elseif ($repoVersionMinor -ne $ProductVersionMinor) { + $versionDrift = 'minor' + } + elseif ($repoVersionPatch -ne $ProductVersionPatch) { + $versionDrift = 'patch' + } + else { + $versionDrift = 'unknown' + } + + $versionDriftSummary = "$repoVersion ($versionDrift)" + $script:azGovVizVersionOnRepositoryFull = $versionDriftSummary + $script:azGovVizNewerVersionAvailable = $true + $script:azGovVizNewerVersionAvailableHTML = 'Get the latest Azure Governance Visualizer version ' + $azGovVizVersionOnRepositoryFull + '! ' + } + else { + Write-Host "Azure Governance Visualizer version is up to date '$ProductVersion'" -ForegroundColor Green + } + } + catch { + #skip + Write-Host 'Azure Governance Visualizer version check skipped' -ForegroundColor Magenta + } +} +function createTagList { + $startTagListArray = Get-Date + Write-Host 'Creating TagList array' + + $tagsSubRgResCount = ($htAllTagList.'AllScopes'.Keys).Count + $tagsSubsriptionCount = ($htAllTagList.'Subscription'.Keys).Count + $tagsResourceGroupCount = ($htAllTagList.'ResourceGroup'.Keys).Count + $tagsResourceCount = ($htAllTagList.'Resource'.Keys).Count + Write-Host " Total Number of ALL unique Tag Names: $tagsSubRgResCount" + Write-Host " Total Number of Subscription unique Tag Names: $tagsSubsriptionCount" + Write-Host " Total Number of ResourceGroup unique Tag Names: $tagsResourceGroupCount" + Write-Host " Total Number of Resource unique Tag Names: $tagsResourceCount" + + foreach ($tagScope in $htAllTagList.keys) { + foreach ($tagScopeTagName in $htAllTagList.($tagScope).keys) { + $null = $script:arrayTagList.Add([PSCustomObject]@{ + Scope = $tagScope + TagName = ($tagScopeTagName) + TagCount = $htAllTagList.($tagScope).($tagScopeTagName) + }) + } + } + $endTagListArray = Get-Date + Write-Host "Creating TagList array duration: $((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalMinutes) minutes ($((New-TimeSpan -Start $startTagListArray -End $endTagListArray).TotalSeconds) seconds)" +} +function detailSubscriptions { + $start = Get-Date + Write-Host 'Subscription picking' + #API in rare cases returns duplicates, therefor sorting unique (id) + $childrenSubscriptions = $arrayEntitiesFromAPI.where( { $_.properties.parentNameChain -contains $ManagementGroupID -and $_.type -eq '/subscriptions' } ) | Sort-Object -Property id -Unique + $script:childrenSubscriptionsCount = ($childrenSubscriptions).Count + $script:subsToProcessInCustomDataCollection = [System.Collections.ArrayList]@() + + if ($htSubscriptionsFromOtherTenants.keys.count -gt 0) { + foreach ($subscriptionExludedOtherTenant in $htSubscriptionsFromOtherTenants.keys) { + $subscriptionExludedOtherTenantDetail = $htSubscriptionsFromOtherTenants.($subscriptionExludedOtherTenant).subDetails + $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ + subscriptionId = $subscriptionExludedOtherTenantDetail.subscriptionId + subscriptionName = $subscriptionExludedOtherTenantDetail.displayName + outOfScopeReason = "Foreign tenant: Id: $($subscriptionExludedOtherTenantDetail.tenantId)" + ManagementGroupId = '' + ManagementGroupName = '' + Level = '' + }) + } + } + + if ($htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.keys.count -gt 0) { + foreach ($subscriptionExludedInEntitiesNotInSubscriptions in $htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.keys) { + $subscriptionExludedInEntitiesNotInSubscriptionsDetail = $htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($subscriptionExludedInEntitiesNotInSubscriptions) + $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ + subscriptionId = $subscriptionExludedInEntitiesNotInSubscriptions + subscriptionName = $subscriptionExludedInEntitiesNotInSubscriptionsDetail.properties.displayName + outOfScopeReason = 'Sub in GetEntities, not in GetSubscriptions' + ManagementGroupId = '' + ManagementGroupName = '' + Level = '' + }) + } + } + + foreach ($childrenSubscription in $childrenSubscriptions) { + + $sub = $htAllSubscriptionsFromAPI.($childrenSubscription.name) + if ($sub.subDetails.subscriptionPolicies.quotaId.startswith('AAD_', 'CurrentCultureIgnoreCase') -or $sub.subDetails.state -ne 'Enabled') { + if (($sub.subDetails.subscriptionPolicies.quotaId).startswith('AAD_', 'CurrentCultureIgnoreCase')) { + $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ + subscriptionId = $childrenSubscription.name + subscriptionName = $childrenSubscription.properties.displayName + outOfScopeReason = "QuotaId: AAD_ (State: $($sub.subDetails.state))" + ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent + ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName + Level = $htSubscriptionsMgPath.($childrenSubscription.name).level + }) + } + if ($sub.subDetails.state -ne 'Enabled') { + $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ + subscriptionId = $childrenSubscription.name + subscriptionName = $childrenSubscription.properties.displayName + outOfScopeReason = "State: $($sub.subDetails.state)" + ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent + ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName + Level = $htSubscriptionsMgPath.($childrenSubscription.name).level + }) + } + } + else { + if ($SubscriptionQuotaIdWhitelist[0] -ne 'undefined') { + $whitelistMatched = 'unknown' + foreach ($subscriptionQuotaIdWhitelistQuotaId in $SubscriptionQuotaIdWhitelist) { + if (($sub.subDetails.subscriptionPolicies.quotaId).startswith($subscriptionQuotaIdWhitelistQuotaId, 'CurrentCultureIgnoreCase')) { + $whitelistMatched = 'inWhitelist' + } + } + + if ($whitelistMatched -eq 'inWhitelist') { + #write-host "$($childrenSubscription.properties.displayName) in whitelist" + $null = $script:subsToProcessInCustomDataCollection.Add([PSCustomObject]@{ + subscriptionId = $childrenSubscription.name + subscriptionName = $childrenSubscription.properties.displayName + subscriptionQuotaId = $sub.subDetails.subscriptionPolicies.quotaId + }) + } + else { + #Write-Host " preCustomDataCollection: $($childrenSubscription.properties.displayName) ($($childrenSubscription.name)) Subscription Quota Id: $($sub.subDetails.subscriptionPolicies.quotaId) is out of scope for Azure Governance Visualizer (not in Whitelist)" + $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ + subscriptionId = $childrenSubscription.name + subscriptionName = $childrenSubscription.properties.displayName + outOfScopeReason = "QuotaId: '$($sub.subDetails.subscriptionPolicies.quotaId)' not in Whitelist" + ManagementGroupId = $htSubscriptionsMgPath.($childrenSubscription.name).Parent + ManagementGroupName = $htSubscriptionsMgPath.($childrenSubscription.name).ParentName + Level = $htSubscriptionsMgPath.($childrenSubscription.name).level + }) + } + } + else { + $null = $script:subsToProcessInCustomDataCollection.Add([PSCustomObject]@{ + subscriptionId = $childrenSubscription.name + subscriptionName = $childrenSubscription.properties.displayName + subscriptionQuotaId = $sub.subDetails.subscriptionPolicies.quotaId + }) + } + } + } + + if ($subsToProcessInCustomDataCollection.Count -lt $childrenSubscriptionsCount) { + Write-Host " $($subsToProcessInCustomDataCollection.Count) of $($childrenSubscriptionsCount) Subscriptions picked for processing" -ForegroundColor yellow + } + else { + Write-Host " $($subsToProcessInCustomDataCollection.Count) of $($childrenSubscriptionsCount) Subscriptions picked for processing" + } + + + if ($outOfScopeSubscriptions.Count -gt 0) { + Write-Host " $($outOfScopeSubscriptions.Count) Subscriptions excluded" -ForegroundColor yellow + $outOfScopeSubscriptionsGroupedByOutOfScopeReason = $outOfScopeSubscriptions | Group-Object -Property outOfScopeReason + foreach ($exclusionreason in $outOfScopeSubscriptionsGroupedByOutOfScopeReason) { + Write-Host " $($exclusionreason.Count): $($exclusionreason.Name) ($($exclusionreason.Group.subscriptionId -join ', '))" + } + + foreach ($outOfScopeSubscription in $outOfScopeSubscriptions) { + $script:htOutOfScopeSubscriptions.($outOfScopeSubscription.subscriptionId) = @{ + subscriptionId = $outOfScopeSubscription.subscriptionId + subscriptionName = $outOfScopeSubscription.subscriptionName + outOfScopeReason = $outOfScopeSubscription.outOfScopeReason + ManagementGroupId = $outOfScopeSubscription.ManagementGroupId + ManagementGroupName = $outOfScopeSubscription.ManagementGroupName + Level = $outOfScopeSubscription.Level + } + } + } + else { + Write-Host " $($outOfScopeSubscriptions.Count) Subscriptions excluded" + } + $script:subsToProcessInCustomDataCollectionCount = ($subsToProcessInCustomDataCollection).Count + + $end = Get-Date + Write-Host "Subscription picking duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" +} +function detectPolicyEffect { + [CmdletBinding()] + Param + ( + [object] + $policyDefinition + ) + + $htEffect = @{ + defaultValue = 'n/a' + allowedValues = 'n/a' + fixedValue = 'n/a' + } + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.policyRule.then.effect)) { + if ($policyDefinition.properties.policyRule.then.effect -in $ValidPolicyEffects) { + # $arrayeffect += "fixed: $($policyDefinition.properties.policyRule.then.effect)" + # return $arrayeffect + $htEffect.fixedValue = $policyDefinition.properties.policyRule.then.effect + return $htEffect + } + else { + $Regex = [Regex]::new("(?<=\[parameters\(')(.*)(?='\)\])") + $Match = $Regex.Match($policyDefinition.properties.policyRule.then.effect) + if ($Match.Success) { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value))) { + + #defaultValue + if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'defaultvalue') { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).defaultValue)) { + if ($policyDefinition.properties.parameters.($Match.Value).defaultValue -in $ValidPolicyEffects) { + #$arrayeffect += "default: $($policyDefinition.properties.parameters.($Match.Value).defaultValue)" + $htEffect.defaultValue = $policyDefinition.properties.parameters.($Match.Value).defaultValue + } + else { + Write-Host "invalid defaultValue effect $($policyDefinition.properties.parameters.($Match.Value).defaultValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "defaultValue empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "finding: Policy has no defaultvalue for effect: $($policyDefinition.id) ($($policyDefinition.properties.policyType))" + } + #allowedValues + if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'allowedValues') { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).allowedValues)) { + if ($policyDefinition.properties.parameters.($Match.Value).allowedValues.Count -gt 0) { + #Write-Host "allowedValues count $($policyDefinition.properties.parameters.($Match.Value).allowedValues) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + $arrayAllowed = @() + foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { + if ($allowedValue -in $ValidPolicyEffects) { + $arrayAllowed += $allowedValue + } + else { + Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + #$arrayeffect += "allowed: $(($arrayAllowed | Sort-Object) -join ', ')" + $htEffect.allowedValues = ($arrayAllowed | Sort-Object) -join ',' + } + } + else { + Write-Host "allowedValues empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "no allowedValues- $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + + } + else { + Write-Host "unexpected - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + + return $htEffect + } + } + } + else { + Write-Host "no then effect - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + return $htEffect +} +function exportBaseCSV { + if (-not $NoCsvExport) { + Write-Host "Exporting CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName).csv'" + $startBuildCSV = Get-Date + + $outprops = $newtable[0].PSObject.Properties.Name + if (-not $HierarchyMapOnly -and -not $HierarchyMapOnlyCustomDataJSON) { + $outprops.Set($outprops.IndexOf('PolicyAssignmentNotScopes'), @{L = 'PolicyAssignmentNotScopes'; E = { ($_.PolicyAssignmentNotScopes -join "$CsvDelimiterOpposite ") } }) + } + if ($CsvExportUseQuotesAsNeeded) { + $newTable | Sort-Object -Property level, mgId, SubscriptionId, PolicyAssignmentId, RoleAssignmentId, BlueprintId, BlueprintAssignmentId | Select-Object -Property $outprops -ExcludeProperty PolicyAssignmentParameters | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded + } + else { + $newTable | Sort-Object -Property level, mgId, SubscriptionId, PolicyAssignmentId, RoleAssignmentId, BlueprintId, BlueprintAssignmentId | Select-Object -Property $outprops -ExcludeProperty PolicyAssignmentParameters | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $endBuildCSV = Get-Date + Write-Host "Exporting CSV total duration: $((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildCSV -End $endBuildCSV).TotalSeconds) seconds)" + } +} +function exportResourceLocks { + $arrayResourceLocks4CSV = [System.Collections.ArrayList]@() + foreach ($sub in $htResourceLocks.Keys) { + $hlper = $htSubscriptionsMgPath.($sub) + $subscriptionDisplayName = $hlper.DisplayName + $mgPath = $hlper.ParentNameChainDelimited + #sub + if ($htResourceLocks.($sub).SubscriptionLocksCannotDeleteCount -eq 1) { + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'Subscription' + Lock = 'CannotDelete' + Id = "/subscriptions/$sub" + ResourceType = 'Microsoft.Resources/subscriptions' + }) + } + if ($htResourceLocks.($sub).SubscriptionLocksReadOnlyCount -eq 1) { + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'Subscription' + Lock = 'ReadOnly' + Id = "/subscriptions/$sub" + ResourceType = 'Microsoft.Resources/subscriptions' + }) + } + #rg + if ($htResourceLocks.($sub).ResourceGroupsLocksCannotDeleteCount -gt 0) { + foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksCannotDelete) { + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'ResourceGroup' + Lock = 'CannotDelete' + Id = $res.rg + ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' + }) + } + } + if ($htResourceLocks.($sub).ResourceGroupsLocksReadOnlyCount -gt 0) { + foreach ($res in $htResourceLocks.($sub).ResourceGroupsLocksReadOnly) { + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'ResourceGroup' + Lock = 'ReadOnly' + Id = $res.rg + ResourceType = 'Microsoft.Resources/subscriptions/resourceGroups' + }) + } + } + #res + if ($htResourceLocks.($sub).ResourcesLocksCannotDeleteCount -gt 0) { + foreach ($res in $htResourceLocks.($sub).ResourcesLocksCannotDelete) { + $resSplit = ($res.res -split '/') + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'Resource' + Lock = 'CannotDelete' + Id = $res.res + ResourceType = "$($resSplit[6])/$($resSplit[7])" + }) + } + } + if ($htResourceLocks.($sub).ResourcesLocksReadOnlyCount -gt 0) { + foreach ($res in $htResourceLocks.($sub).ResourcesLocksReadOnly) { + $resSplit = ($res.res -split '/') + $null = $arrayResourceLocks4CSV.Add([PSCustomObject]@{ + SubscriptionId = $sub + SubscriptionName = $subscriptionDisplayName + MGPath = $mgPath + ScopeType = 'Resource' + Lock = 'ReadOnly' + Id = $res.res + ResourceType = "$($resSplit[6])/$($resSplit[7])" + }) + } + } + } + if ($arrayResourceLocks4CSV.count -gt 0) { + if (-not $NoCsvExport) { + Write-Host "Exporting ResourceLocks CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv'" + $arrayResourceLocks4CSV | Sort-Object -Property ScopeType, Lock, SubscriptionId, Id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceLocks.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } +} +function getConsumption { + + function addToAllConsumptionData { + [CmdletBinding()]Param( + [Parameter(Mandatory)] + [object] + $consumptiondataFromAPI + ) + + foreach ($consumptionline in $consumptiondataFromAPI.properties.rows) { + $hlper = $htSubscriptionsMgPath.($consumptionline[1]) + + $null = $script:allConsumptionData.Add([PSCustomObject]@{ + "$($consumptiondataFromAPI.properties.columns.name[0])" = [decimal]$consumptionline[0] + "$($consumptiondataFromAPI.properties.columns.name[1])" = $consumptionline[1] + SubscriptionName = $hlper.DisplayName + SubscriptionMgPath = $hlper.ParentNameChainDelimited + "$($consumptiondataFromAPI.properties.columns.name[2])" = $consumptionline[2] + "$($consumptiondataFromAPI.properties.columns.name[3])" = $consumptionline[3] + "$($consumptiondataFromAPI.properties.columns.name[4])" = $consumptionline[4] + "$($consumptiondataFromAPI.properties.columns.name[5])" = $consumptionline[5] + "$($consumptiondataFromAPI.properties.columns.name[6])" = $consumptionline[6] + }) + } + } + + $startConsumptionData = Get-Date + + #cost only for whitelisted quotaId + if ($SubscriptionQuotaIdWhitelist[0] -ne 'undefined') { + if ($subsToProcessInCustomDataCollectionCount -gt 0) { + #region mgScopeWhitelisted + #$subscriptionIdsOptimizedForBody = '"{0}"' -f ($subsToProcessInCustomDataCollection.subscriptionId -join '","') + $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for $($subsToProcessInCustomDataCollectionCount) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" + Write-Host "$currentTask" + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" + $method = 'POST' + + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 100 + $subscriptionsBatch = ($subsToProcessInCustomDataCollection | Sort-Object -Property subscriptionQuotaId) | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + $batchCnt = 0 + + foreach ($batch in $subscriptionsBatch) { + $batchCnt++ + $subscriptionIdsOptimizedForBody = '"{0}"' -f (($batch.Group).subscriptionId -join '","') + $currenttask = "Getting Consumption data #batch$($batchCnt)/$(($subscriptionsBatch | Measure-Object).Count) (scope MG '$($ManagementGroupId)') for $(($batch.Group).Count) Subscriptions (QuotaId Whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" + Write-Host "$currentTask" -ForegroundColor Cyan + + $body = @" +{ +"type": "ActualCost", +"dataset": { + "granularity": "none", + "filter": { + "dimensions": { + "name": "SubscriptionId", + "operator": "In", + "values": [ + $($subscriptionIdsOptimizedForBody) + ] + } + }, + "aggregation": { + "totalCost": { + "name": "PreTaxCost", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "SubscriptionId" + }, + { + "type": "Dimension", + "name": "ResourceId" + }, + { + "type": "Dimension", + "name": "ResourceType" + }, + { + "type": "Dimension", + "name": "MeterCategory" + }, + { + "type": "Dimension", + "name": "ChargeType" + } + ] +}, +"timeframe": "Custom", +"timeperiod": { + "from": "$($azureConsumptionStartDate)", + "to": "$($azureConsumptionEndDate)" +} +} +"@ + + $mgConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' + #endregion mgScopeWhitelisted + + <#test + #$mgConsumptionData = "OfferNotSupported" + if ($batchCnt -eq 1){ + $mgConsumptionData = "OfferNotSupported" + } + #> + + if ($mgConsumptionData -eq 'Unauthorized' -or $mgConsumptionData -eq 'OfferNotSupported' -or $mgConsumptionData -eq 'NoValidSubscriptions') { + if (-not $script:htConsumptionExceptionLog.Mg.($ManagementGroupId)) { + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId) = @{} + } + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt) = @{} + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt).Exception = $mgConsumptionData + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).($batchCnt).Subscriptions = ($batch.Group).subscriptionId + Write-Host " Switching to 'foreach Subscription' Subscription scope mode. Getting Consumption data #batch$($batchCnt) using Management Group scope failed." + #region subScopewhitelisted + $body = @" +{ +"type": "ActualCost", +"dataset": { + "granularity": "none", + "aggregation": { + "totalCost": { + "name": "PreTaxCost", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "SubscriptionId" + }, + { + "type": "Dimension", + "name": "ResourceId" + }, + { + "type": "Dimension", + "name": "ResourceType" + }, + { + "type": "Dimension", + "name": "MeterCategory" + }, + { + "type": "Dimension", + "name": "ChargeType" + } + ] +}, +"timeframe": "Custom", +"timeperiod": { + "from": "$($azureConsumptionStartDate)", + "to": "$($azureConsumptionEndDate)" +} +} +"@ + $funcAddToAllConsumptionData = $function:addToAllConsumptionData.ToString() + $batch.Group | ForEach-Object -Parallel { + $subIdToProcess = $_.subscriptionId + $subNameToProcess = $_.subscriptionName + $subscriptionQuotaIdToProcess = $_.subscriptionQuotaId + #region UsingVARs + $body = $using:body + $azureConsumptionStartDate = $using:azureConsumptionStartDate + $azureConsumptionEndDate = $using:azureConsumptionEndDate + $SubscriptionQuotaIdWhitelist = $using:SubscriptionQuotaIdWhitelist + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $allConsumptionData = $using:allConsumptionData + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $htConsumptionExceptionLog = $using:htConsumptionExceptionLog + #other + $function:addToAllConsumptionData = $using:funcAddToAllConsumptionData + #endregion UsingVARs + + $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" + #test + Write-Host $currentTask + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" + $method = 'POST' + $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' + if ($subConsumptionData -eq 'Unauthorized' -or $subConsumptionData -eq 'OfferNotSupported' -or $subConsumptionData -eq 'InvalidQueryDefinition' -or $subConsumptionData -eq 'NonValidWebDirectAIRSOfferType' -or $subConsumptionData -eq 'NotFoundNotSupported' -or $subConsumptionData -eq 'IndirectCostDisabled') { + Write-Host " Failed ($subConsumptionData) - Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)) (whitelist))" + $hlper = $htAllSubscriptionsFromAPI.($subIdToProcess).subDetails + $hlper2 = $htSubscriptionsMgPath.($subIdToProcess) + $script:htConsumptionExceptionLog.Sub.($subIdToProcess) = @{} + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).Exception = $subConsumptionData + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionId = $subIdToProcess + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionName = $hlper.displayName + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).QuotaId = $hlper.subscriptionPolicies.quotaId + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgPath = $hlper2.ParentNameChainDelimited + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgParent = $hlper2.Parent + Continue + } + else { + Write-Host " $($subConsumptionData.Count) Consumption data entries ((scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess))))" + if ($subConsumptionData.Count -gt 0) { + addToAllConsumptionData -consumptiondataFromAPI $subConsumptionData + <# + foreach ($consumptionEntry in $subConsumptionData) { + if ($consumptionEntry.PreTaxCost -ne 0) { + $null = $script:allConsumptionData.Add($consumptionEntry) + } + } + #> + + } + } + } -ThrottleLimit $ThrottleLimit + #endregion subScopewhitelisted + } + else { + Write-Host " $($mgConsumptionData.Count) Consumption data entries" + if ($mgConsumptionData.Count -gt 0) { + addToAllConsumptionData -consumptiondataFromAPI $mgConsumptionData + <# + foreach ($consumptionEntry in $mgConsumptionData) { + if ($consumptionEntry.PreTaxCost -ne 0) { + $null = $script:allConsumptionData.Add($consumptionEntry) + } + } + #> + } + } + } + } + else { + $detailShowStopperResult = 'NoWhitelistSubscriptionsPresent' + Write-Host ' No Subscriptions matching whitelist present, skipping Consumption data processing' + #überprüfen + } + } + else { + + if ($subsToProcessInCustomDataCollectionCount -gt 0) { + #region mgScope + $currenttask = "Getting Consumption data (scope MG '$($ManagementGroupId)') for period $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" + Write-Host "$currentTask" + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" + $method = 'POST' + $body = @" +{ + "type": "ActualCost", + "dataset": { + "granularity": "none", + "aggregation": { + "totalCost": { + "name": "PreTaxCost", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "SubscriptionId" + }, + { + "type": "Dimension", + "name": "ResourceId" + }, + { + "type": "Dimension", + "name": "ResourceType" + }, + { + "type": "Dimension", + "name": "MeterCategory" + }, + { + "type": "Dimension", + "name": "ChargeType" + } + ] + }, + "timeframe": "Custom", + "timeperiod": { + "from": "$($azureConsumptionStartDate)", + "to": "$($azureConsumptionEndDate)" + } +} +"@ + #$script:allConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' + $allConsumptionDataAPIResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' + #endregion mgScope + + #test + #$allConsumptionData = "OfferNotSupported" + + if ($allConsumptionDataAPIResult -eq 'AccountCostDisabled' <#-or $allConsumptionDataAPIResult -eq 'NoValidSubscriptions'#>) { + if ($allConsumptionDataAPIResult -eq 'AccountCostDisabled') { + $detailShowStopperResult = $allConsumptionDataAPIResult + } + <#if ($allConsumptionDataAPIResult -eq 'NoValidSubscriptions') { + $detailShowStopperResult = $allConsumptionDataAPIResult + }#> + } + else { + if ($allConsumptionDataAPIResult -eq 'Unauthorized' -or $allConsumptionDataAPIResult -eq 'OfferNotSupported' -or $allConsumptionDataAPIResult -eq 'NoValidSubscriptions' -or $allConsumptionDataAPIResult -eq 'tooManySubscriptions') { + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId) = @{} + $script:htConsumptionExceptionLog.Mg.($ManagementGroupId).Exception = $allConsumptionDataAPIResult + Write-Host " Switching to 'foreach Subscription' mode. Getting Consumption data using Management Group scope failed." + #region subScope + $body = @" +{ + "type": "ActualCost", + "dataset": { + "granularity": "none", + "aggregation": { + "totalCost": { + "name": "PreTaxCost", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "SubscriptionId" + }, + { + "type": "Dimension", + "name": "ResourceId" + }, + { + "type": "Dimension", + "name": "ResourceType" + }, + { + "type": "Dimension", + "name": "MeterCategory" + }, + { + "type": "Dimension", + "name": "ChargeType" + } + ] + }, + "timeframe": "Custom", + "timeperiod": { + "from": "$($azureConsumptionStartDate)", + "to": "$($azureConsumptionEndDate)" + } +} +"@ + + $funcAddToAllConsumptionData = $function:addToAllConsumptionData.ToString() + $subsToProcessInCustomDataCollection | ForEach-Object -Parallel { + $subIdToProcess = $_.subscriptionId + $subNameToProcess = $_.subscriptionName + $subscriptionQuotaIdToProcess = $_.subscriptionQuotaId + #region UsingVARs + $body = $using:body + $azureConsumptionStartDate = $using:azureConsumptionStartDate + $azureConsumptionEndDate = $using:azureConsumptionEndDate + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $allConsumptionData = $using:allConsumptionData + $htConsumptionExceptionLog = $using:htConsumptionExceptionLog + #other + $function:addToAllConsumptionData = $using:funcAddToAllConsumptionData + #endregion UsingVARs + + $currentTask = " Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" + #test + Write-Host $currentTask + #https://docs.microsoft.com/en-us/rest/api/cost-management/query/usage + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($subIdToProcess)/providers/Microsoft.CostManagement/query?api-version=2019-11-01&`$top=5000" + $method = 'POST' + $subConsumptionData = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask -listenOn 'ContentProperties' + if ($subConsumptionData -eq 'Unauthorized' -or $subConsumptionData -eq 'OfferNotSupported' -or $subConsumptionData -eq 'InvalidQueryDefinition' -or $subConsumptionData -eq 'NonValidWebDirectAIRSOfferType' -or $subConsumptionData -eq 'NotFoundNotSupported' -or $subConsumptionData -eq 'IndirectCostDisabled') { + Write-Host " Failed ($subConsumptionData) - Getting Consumption data (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" + $hlper = $htAllSubscriptionsFromAPI.($subIdToProcess).subDetails + $hlper2 = $htSubscriptionsMgPath.($subIdToProcess) + $script:htConsumptionExceptionLog.Sub.($subIdToProcess) = @{} + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).Exception = $subConsumptionData + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionId = $subIdToProcess + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).SubscriptionName = $hlper.displayName + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).QuotaId = $hlper.subscriptionPolicies.quotaId + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgPath = $hlper2.ParentNameChainDelimited + $script:htConsumptionExceptionLog.Sub.($subIdToProcess).mgParent = $hlper2.Parent + Continue + } + else { + Write-Host " $($subConsumptionData.Count) Consumption data entries (scope Sub $($subNameToProcess) '$($subIdToProcess)' ($($subscriptionQuotaIdToProcess)))" + if ($subConsumptionData.Count -gt 0) { + addToAllConsumptionData -consumptiondataFromAPI $subConsumptionData + <# + foreach ($consumptionEntry in $subConsumptionData) { + if ($consumptionEntry.PreTaxCost -ne 0) { + $null = $script:allConsumptionData.Add($consumptionEntry) + } + } + #> + } + } + } -ThrottleLimit $ThrottleLimit + #endregion subScope + } + else { + Write-Host " $($allConsumptionDataAPIResult.properties.rows.Count) Consumption data entries" + if ($allConsumptionDataAPIResult.properties.rows.Count -gt 0) { + addToAllConsumptionData -consumptiondataFromAPI $allConsumptionDataAPIResult + } + } + } + } + else { + $detailShowStopperResult = 'NoSubscriptionsPresent' + Write-Host ' No Subscriptions present, skipping Consumption data processing' + } + } + + if ($detailShowStopperResult -eq 'AccountCostDisabled' -or $detailShowStopperResult -eq 'NoValidSubscriptions' -or $detailShowStopperResult -eq 'NoWhitelistSubscriptionsPresent' -or $detailShowStopperResult -eq 'NoSubscriptionsPresent') { + if ($detailShowStopperResult -eq 'AccountCostDisabled') { + Write-Host ' Seems Access to cost data has been disabled for this Account - skipping CostManagement' + } + if ($detailShowStopperResult -eq 'NoValidSubscriptions') { + Write-Host ' Seems there are no valid Subscriptions present - skipping CostManagement' + } + if ($detailShowStopperResult -eq 'NoWhitelistSubscriptionsPresent') { + Write-Host " Seems there are no Subscriptions present that match the whitelist ($($SubscriptionQuotaIdWhitelist -join ', ')) - skipping CostManagement" + } + if ($detailShowStopperResult -eq 'NoSubscriptionsPresent') { + Write-Host ' Seems there are no Subscriptions present - skipping CostManagement' + } + Write-Host " Action: Setting switch parameter 'DoAzureConsumption' to false" + $azAPICallConf['htParameters'].DoAzureConsumption = $false + } + else { + Write-Host ' Checking returned Consumption data' + $script:allConsumptionDataCount = $allConsumptionData.Count + + if ($allConsumptionDataCount -gt 0) { + + $script:allConsumptionData = $allConsumptionData.where( { $_.PreTaxCost -ne 0 } ) + $script:allConsumptionDataCount = $allConsumptionData.Count + + if ($allConsumptionDataCount -gt 0) { + Write-Host " $($allConsumptionDataCount) relevant Consumption data entries" + + $script:consumptionData = $allConsumptionData + $script:consumptionDataGroupedByCurrency = $consumptionData | Group-Object -Property Currency + + foreach ($currency in $consumptionDataGroupedByCurrency) { + + #subscriptions + $groupAllConsumptionDataPerCurrencyBySubscriptionId = $currency.group | Group-Object -Property SubscriptionId + foreach ($subscriptionId in $groupAllConsumptionDataPerCurrencyBySubscriptionId) { + + $subTotalCost = ($subscriptionId.Group.PreTaxCost | Measure-Object -Sum).Sum + $script:htAzureConsumptionSubscriptions.($subscriptionId.Name) = @{} + $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).ConsumptionData = $subscriptionId.group + $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).TotalCost = $subTotalCost + $script:htAzureConsumptionSubscriptions.($subscriptionId.Name).Currency = $currency.Name + $resourceTypes = $subscriptionId.Group.ResourceType | Sort-Object -Unique + + foreach ($parentMg in $htSubscriptionsMgPath.($subscriptionId.Name).ParentNameChain) { + + if (-not $htManagementGroupsCost.($parentMg)) { + $script:htManagementGroupsCost.($parentMg) = @{} + $script:htManagementGroupsCost.($parentMg).currencies = $currency.Name + $script:htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" = $subTotalCost #[decimal]$subTotalCost + $script:htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" = ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count + $script:htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent = ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count + $script:htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" = 1 + $script:htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent = 1 + $script:htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" = $resourceTypes + $script:htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent = $resourceTypes + $script:htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" = $subscriptionId.group + $script:htManagementGroupsCost.($parentMg).consumptionDataSubscriptions = $subscriptionId.group + } + else { + $newMgTotalCost = $htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" + $subTotalCost #[decimal]$subTotalCost + $script:htManagementGroupsCost.($parentMg)."mgTotalCost_$($currency.Name)" = $newMgTotalCost #[decimal]$newMgTotalCost + + $currencies = [array]$htManagementGroupsCost.($parentMg).currencies + if ($currencies -notcontains $currency.Name) { + $currencies += $currency.Name + $script:htManagementGroupsCost.($parentMg).currencies = $currencies + } + + #currency based + $resourcesThatGeneratedCost = $htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" + ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count + $script:htManagementGroupsCost.($parentMg)."resourcesThatGeneratedCost_$($currency.Name)" = $resourcesThatGeneratedCost + + $subscriptionsThatGeneratedCost = $htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" + 1 + $script:htManagementGroupsCost.($parentMg)."subscriptionsThatGeneratedCost_$($currency.Name)" = $subscriptionsThatGeneratedCost + + $consumptionDataSubscriptions = $htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" += $subscriptionId.group + $script:htManagementGroupsCost.($parentMg)."consumptionDataSubscriptions_$($currency.Name)" = $consumptionDataSubscriptions + + $resourceTypesThatGeneratedCost = $htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" + foreach ($resourceType in $resourceTypes) { + if ($resourceTypesThatGeneratedCost -notcontains $resourceType) { + $resourceTypesThatGeneratedCost += $resourceType + } + } + $script:htManagementGroupsCost.($parentMg)."resourceTypesThatGeneratedCost_$($currency.Name)" = $resourceTypesThatGeneratedCost + + #currencyIndependent + $resourcesThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent + ($subscriptionId.Group.ResourceId | Sort-Object -Unique | Measure-Object).Count + $script:htManagementGroupsCost.($parentMg).resourcesThatGeneratedCostCurrencyIndependent = $resourcesThatGeneratedCostCurrencyIndependent + + $subscriptionsThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent + 1 + $script:htManagementGroupsCost.($parentMg).subscriptionsThatGeneratedCostCurrencyIndependent = $subscriptionsThatGeneratedCostCurrencyIndependent + + $consumptionDataSubscriptionsCurrencyIndependent = $htManagementGroupsCost.($parentMg).consumptionDataSubscriptions += $subscriptionId.group + $script:htManagementGroupsCost.($parentMg).consumptionDataSubscriptions = $consumptionDataSubscriptionsCurrencyIndependent + + $resourceTypesThatGeneratedCostCurrencyIndependent = $htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent + foreach ($resourceType in $resourceTypes) { + if ($resourceTypesThatGeneratedCostCurrencyIndependent -notcontains $resourceType) { + $resourceTypesThatGeneratedCostCurrencyIndependent += $resourceType + } + } + $script:htManagementGroupsCost.($parentMg).resourceTypesThatGeneratedCostCurrencyIndependent = $resourceTypesThatGeneratedCostCurrencyIndependent + } + } + } + + $totalCost = 0 + $script:tenantSummaryConsumptionDataGrouped = $currency.group | Group-Object -Property ResourceType, ChargeType, MeterCategory + $subsCount = ($tenantSummaryConsumptionDataGrouped.group.subscriptionId | Sort-Object -Unique | Measure-Object).Count + $consumedServiceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceType | Sort-Object -Unique | Measure-Object).Count + $resourceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceId | Sort-Object -Unique | Measure-Object).Count + foreach ($consumptionline in $tenantSummaryConsumptionDataGrouped) { + + $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum + + if ([math]::Round($costConsumptionLine, 2) -eq 0) { + $cost = $costConsumptionLine.ToString('0.0000') + } + else { + $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') + } + + $null = $script:arrayConsumptionData.Add([PSCustomObject]@{ + ResourceType = ($consumptionline.name).split(', ')[0] + ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] + ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] + ConsumedServiceInstanceCount = $consumptionline.Count + ConsumedServiceCost = $cost #[decimal]$cost + ConsumedServiceSubscriptions = ($consumptionline.group.SubscriptionId | Sort-Object -Unique).Count + ConsumedServiceCurrency = $currency.Name + }) + + $totalCost = $totalCost + $costConsumptionLine + + } + if ([math]::Round($totalCost, 2) -eq 0) { + $totalCost = $totalCost + } + else { + $totalCost = [math]::Round($totalCost, 2).ToString('0.00') + } + $script:arrayTotalCostSummary += "$($totalCost) $($currency.Name) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes) in $($subsCount) Subscriptions" + } + } + else { + Write-Host ' No relevant consumption data entries (0)' + } + } + + #region BuildConsumptionCSV + if (-not $NoCsvExport) { + if (-not $NoAzureConsumptionReportExportToCSV) { + Write-Host " Exporting Consumption CSV $($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" + $startBuildConsumptionCSV = Get-Date + if ($CsvExportUseQuotesAsNeeded) { + $allConsumptionData | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded + } + else { + $allConsumptionData | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_Consumption.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + $endBuildConsumptionCSV = Get-Date + Write-Host " Exporting Consumption CSV total duration: $((New-TimeSpan -Start $startBuildConsumptionCSV -End $endBuildConsumptionCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildConsumptionCSV -End $endBuildConsumptionCSV).TotalSeconds) seconds)" + } + } + #endregion BuildConsumptionCSV + } + $endConsumptionData = Get-Date + Write-Host "Getting Consumption data duration: $((New-TimeSpan -Start $startConsumptionData -End $endConsumptionData).TotalSeconds) seconds" +} +function getDefaultManagementGroup { + $currentTask = 'Get Default Management Group' + Write-Host $currentTask + #https://docs.microsoft.com/en-us/azure/governance/management-groups/how-to/protect-resource-hierarchy#setting---default-management-group + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($azAPICallConf['checkContext'].Tenant.Id)/settings?api-version=2020-02-01" + $method = 'GET' + $settingsMG = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if (($settingsMG).count -gt 0) { + Write-Host " default ManagementGroup Id: $($settingsMG.properties.defaultManagementGroup)" + $script:defaultManagementGroupId = $settingsMG.properties.defaultManagementGroup + Write-Host " requireAuthorizationForGroupCreation: $($settingsMG.properties.requireAuthorizationForGroupCreation)" + $script:requireAuthorizationForGroupCreation = $settingsMG.properties.requireAuthorizationForGroupCreation + } + else { + Write-Host " default ManagementGroup: $(($azAPICallConf['checkContext']).Tenant.Id) (Tenant Root)" + $script:defaultManagementGroupId = ($azAPICallConf['checkContext']).Tenant.Id + $script:requireAuthorizationForGroupCreation = $false + } +} +function getEntities { + Write-Host 'Entities' + $startEntities = Get-Date + $currentTask = ' Getting Entities' + Write-Host $currentTask + #https://management.azure.com/providers/Microsoft.Management/getEntities?api-version=2020-02-01 + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/getEntities?api-version=2020-02-01" + $method = 'POST' + $arrayEntitiesFromAPIInitial = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + Write-Host " $($arrayEntitiesFromAPIInitial.Count) Entities returned" + + $script:arrayEntitiesFromAPI = [System.Collections.ArrayList]@() + $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions = @{} + foreach ($entry in $arrayEntitiesFromAPIInitial) { + if ($entry.Type -eq '/subscriptions') { + if ($htSubscriptionsFromOtherTenants.($entry.name)) { + $subdetail = $htSubscriptionsFromOtherTenants.($entry.name).subdetails + Write-Host " Excluded Subscription '$($subDetail.displayName)' ($($entry.name)) (foreign tenantId: '$($subDetail.tenantId)')" -ForegroundColor DarkRed + continue + } + if (-not $htAllSubscriptionsFromAPI.($entry.name)) { + #not contained in subscriptions + $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry + Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed + continue + } + #test + # if ($entry.name -eq '') { + # $script:htsubscriptionsFromEntitiesThatAreNotInGetSubscriptions.($entry.name) = $entry + # Write-Host " Excluded Subscription '$($entry.properties.displayName)' ($($entry.name)) (contained in GetEntities, not contained in GetSubscriptions)" -ForegroundColor DarkRed + # continue + # } + } + + $null = $script:arrayEntitiesFromAPI.Add($entry) + } + + Write-Host " $($arrayEntitiesFromAPI.Count)/$($arrayEntitiesFromAPIInitial.Count) Entities relevant" + + $endEntities = Get-Date + Write-Host " Getting Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" + + $startEntitiesdata = Get-Date + Write-Host ' Processing Entities data' + $script:htSubscriptionsMgPath = @{} + $script:htManagementGroupsMgPath = @{} + $script:htEntities = @{} + $script:htEntitiesPlain = @{} + + foreach ($entity in $arrayEntitiesFromAPI) { + $script:htEntitiesPlain.($entity.Name) = @{} + $script:htEntitiesPlain.($entity.Name) = $entity + } + + foreach ($entity in $arrayEntitiesFromAPI) { + if ($entity.Type -eq '/subscriptions') { + $parent = $entity.properties.parent.Id -replace '.*/' + $parentId = $entity.properties.parent.Id + $script:htSubscriptionsMgPath.($entity.name) = @{} + $script:htSubscriptionsMgPath.($entity.name).ParentNameChain = $entity.properties.parentNameChain + $script:htSubscriptionsMgPath.($entity.name).ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' + $script:htSubscriptionsMgPath.($entity.name).Parent = $entity.properties.parent.Id -replace '.*/' + $script:htSubscriptionsMgPath.($entity.name).ParentName = $htEntitiesPlain.($entity.properties.parent.Id -replace '.*/').properties.displayName + $script:htSubscriptionsMgPath.($entity.name).DisplayName = $entity.properties.displayName + $array = $entity.properties.parentNameChain + $array += $entity.name + $script:htSubscriptionsMgPath.($entity.name).path = $array + $script:htSubscriptionsMgPath.($entity.name).pathDelimited = $array -join '/' + $script:htSubscriptionsMgPath.($entity.name).level = (($entity.properties.parentNameChain).Count - 1) + } + if ($entity.Type -eq 'Microsoft.Management/managementGroups') { + if ([string]::IsNullOrEmpty($entity.properties.parent.Id)) { + $parent = '__TenantRoot__' + $parentId = '__TenantRoot__' + } + else { + $parent = $entity.properties.parent.Id -replace '.*/' + $parentId = $entity.properties.parent.Id + } + $script:htManagementGroupsMgPath.($entity.name) = @{} + $script:htManagementGroupsMgPath.($entity.name).ParentNameChain = $entity.properties.parentNameChain + $script:htManagementGroupsMgPath.($entity.name).ParentNameChainDelimited = $entity.properties.parentNameChain -join '/' + $script:htManagementGroupsMgPath.($entity.name).ParentNameChainCount = ($entity.properties.parentNameChain | Measure-Object).Count + $script:htManagementGroupsMgPath.($entity.name).Parent = $parent + $script:htManagementGroupsMgPath.($entity.name).ChildMgsAll = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.ParentNameChain -contains $entity.name } )).Name + $script:htManagementGroupsMgPath.($entity.name).ChildMgsDirect = ($arrayEntitiesFromAPI.where( { $_.Type -eq 'Microsoft.Management/managementGroups' -and $_.properties.Parent.Id -replace '.*/' -eq $entity.name } )).Name + $script:htManagementGroupsMgPath.($entity.name).DisplayName = $entity.properties.displayName + $script:htManagementGroupsMgPath.($entity.name).Id = ($entity.name) + $array = $entity.properties.parentNameChain + $array += $entity.name + $script:htManagementGroupsMgPath.($entity.name).path = $array + $script:htManagementGroupsMgPath.($entity.name).pathDelimited = $array -join '/' + $script:htManagementGroupsMgPath.($entity.name).level = $array.Count + } + + $script:htEntities.($entity.name) = @{} + $script:htEntities.($entity.name).ParentNameChain = $entity.properties.parentNameChain + $script:htEntities.($entity.name).Parent = $parent + $script:htEntities.($entity.name).ParentId = $parentId + if ($parent -eq '__TenantRoot__') { + $parentDisplayName = '__TenantRoot__' + } + else { + $parentDisplayName = $htEntitiesPlain.($htEntities.($entity.name).Parent).properties.displayName + } + $script:htEntities.($entity.name).ParentDisplayName = $parentDisplayName + $script:htEntities.($entity.name).DisplayName = $entity.properties.displayName + $script:htEntities.($entity.name).Id = $entity.Name + $script:htEntities.($entity.name).Type = $entity.Type + } + + Write-Host " $(($htManagementGroupsMgPath.Keys).Count) relevant Management Groups" + Write-Host " $(($htSubscriptionsMgPath.Keys).Count) relevant Subscriptions" + + $endEntitiesdata = Get-Date + Write-Host " Processing Entities data duration: $((New-TimeSpan -Start $startEntitiesdata -End $endEntitiesdata).TotalSeconds) seconds" + + $script:arrayEntitiesFromAPISubscriptionsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq '/subscriptions' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count + $script:arrayEntitiesFromAPIManagementGroupsCount = ($arrayEntitiesFromAPI.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.properties.parentNameChain -contains $ManagementGroupId } ) | Sort-Object -Property id -Unique).count + 1 + + $endEntities = Get-Date + Write-Host "Processing Entities duration: $((New-TimeSpan -Start $startEntities -End $endEntities).TotalSeconds) seconds" +} +function getFileNaming { + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { + if ($HierarchyMapOnly) { + $script:fileName = "AzGovViz_HierarchyMapOnly_$($ManagementGroupId)" + } + elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { + $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ManagementGroupId)" + } + else { + $script:fileName = "AzGovViz_$($ManagementGroupId)" + } + } + else { + if ($HierarchyMapOnly) { + $script:fileName = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" + } + elseif ($azAPICallConf['htParameters'].ManagementGroupsOnly -eq $true) { + $script:fileName = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" + } + else { + $script:fileName = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)" + } + } +} + +function getGroupmembers($aadGroupId, $aadGroupDisplayName) { + if (-not $htAADGroupsDetails.($aadGroupId)) { + $script:htAADGroupsDetails.$aadGroupId = @{} + $script:htAADGroupsDetails.($aadGroupId).Id = $aadGroupId + $script:htAADGroupsDetails.($aadGroupId).displayname = $aadGroupDisplayName + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupId)/transitiveMembers" + $method = 'GET' + $aadGroupMembers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupmembers $($aadGroupId)" + + if ($aadGroupMembers -eq 'Request_ResourceNotFound') { + $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ + groupId = $aadGroupId + }) + } + + $aadGroupMembersAll = ($aadGroupMembers) + $aadGroupMembersUsers = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.user' } ) + $aadGroupMembersGroups = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.group' } ) + $aadGroupMembersServicePrincipals = $aadGroupMembers.where( { $_.'@odata.type' -eq '#microsoft.graph.servicePrincipal' } ) + + $aadGroupMembersAllCount = $aadGroupMembersAll.count + $aadGroupMembersUsersCount = $aadGroupMembersUsers.count + $aadGroupMembersGroupsCount = $aadGroupMembersGroups.count + $aadGroupMembersServicePrincipalsCount = $aadGroupMembersServicePrincipals.count + #for SP stuff + if ($aadGroupMembersServicePrincipalsCount -gt 0) { + foreach ($identity in $aadGroupMembersServicePrincipals) { + $arrayIdentityObject = [System.Collections.ArrayList]@() + if ($identity.servicePrincipalType -eq 'Application') { + if ($identity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = 'SP APP INT' + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + else { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = 'SP APP EXT' + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + } + elseif ($identity.servicePrincipalType -eq 'ManagedIdentity') { + $miType = 'unknown' + if ($identity.alternativeNames) { + foreach ($altName in $identity.alternativeNames) { + if ($altName -like 'isExplicit=*') { + $splitAltName = $altName.split('=') + if ($splitAltName[1] -eq 'true') { + $miType = 'Usr' + } + if ($splitAltName[1] -eq 'false') { + $miType = 'Sys' + } + } + } + } + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = "SP MI $miType" + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + else { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'servicePrincipal' + spTypeConcatinated = "SP $($identity.servicePrincipalType)" + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + if (-not $htServicePrincipals.($identity.id)) { + #Write-Host "$($identity.displayName) $($identity.id) added - - - - - - - - " + $script:htServicePrincipals.($identity.id) = @{} + $script:htServicePrincipals.($identity.id) = $arrayIdentityObject + } + } + } + + #guests + if ($aadGroupMembersUsersCount -gt 0) { + $cntx = 0 + $cnty = 0 + foreach ($aadGroupMembersUser in $aadGroupMembersUsers | Sort-Object -Property id -Unique) { + $cntx++ + if ($aadGroupMembersUser.userType -eq 'Guest') { + if (-not $htUserTypesGuest.($aadGroupMembersUser.id)) { + $cnty++ + #Write-Host "$($aadGroupMembersUser.id) is Guest" + $script:htUserTypesGuest.($aadGroupMembersUser.id) = @{} + $script:htUserTypesGuest.($aadGroupMembersUser.id).userType = 'Guest' + } + else { + #Write-Host "$($aadGroupMembersUser.id) already known as Guest" + } + } + } + } + + $script:htAADGroupsDetails.($aadGroupId).MembersAllCount = $aadGroupMembersAllCount + $script:htAADGroupsDetails.($aadGroupId).MembersUsersCount = $aadGroupMembersUsersCount + $script:htAADGroupsDetails.($aadGroupId).MembersGroupsCount = $aadGroupMembersGroupsCount + $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipalsCount = $aadGroupMembersServicePrincipalsCount + + if ($aadGroupMembersAllCount -gt 0) { + $script:htAADGroupsDetails.($aadGroupId).MembersAll = $aadGroupMembersAll + + if ($aadGroupMembersUsersCount -gt 0) { + $script:htAADGroupsDetails.($aadGroupId).MembersUsers = $aadGroupMembersUsers + } + if ($aadGroupMembersGroupsCount -gt 0) { + $script:htAADGroupsDetails.($aadGroupId).MembersGroups = $aadGroupMembersGroups + } + if ($aadGroupMembersServicePrincipalsCount -gt 0) { + $script:htAADGroupsDetails.($aadGroupId).MembersServicePrincipals = $aadGroupMembersServicePrincipals + } + } + } +} +function getMDfCSecureScoreMG { + $start = Get-Date + $currentTask = 'Getting Microsoft Defender for Cloud Secure Score for Management Groups' + Write-Host $currentTask + #ref: https://docs.microsoft.com/en-us/azure/governance/management-groups/resource-graph-samples?tabs=azure-cli#secure-score-per-management-group + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" + $method = 'POST' + + $query = @' + SecurityResources + | where type == 'microsoft.security/securescores' + | project subscriptionId, + subscriptionTotal = iff(properties.score.max == 0, 0.00, round(tolong(properties.weight) * todouble(properties.score.current)/tolong(properties.score.max),2)), + weight = tolong(iff(properties.weight == 0, 1, properties.weight)) + | join kind=leftouter ( + ResourceContainers + | where type == 'microsoft.resources/subscriptions' and properties.state == 'Enabled' + | project subscriptionId, mgChain=properties.managementGroupAncestorsChain ) + on subscriptionId + | mv-expand mg=mgChain + | summarize sumSubs = sum(subscriptionTotal), sumWeight = sum(weight), resultsNum = count() by tostring(mg.displayName), mgId = tostring(mg.name) + | extend secureScore = iff(tolong(resultsNum) == 0, 404.00, round(sumSubs/sumWeight*100,2)) + | project mgDisplayName=mg_displayName, mgId, sumSubs, sumWeight, resultsNum, secureScore + | order by mgDisplayName asc +'@ + + $body = @" + { + "query": "$($query)", + "managementGroups":[ + "$($ManagementGroupId)" + ] + } +"@ + + $getMgAscSecureScore = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' + + if ($getMgAscSecureScore) { + if ($getMgAscSecureScore -eq 'capitulation') { + Write-Host ' Microsoft Defender for Cloud SecureScore for Management Groups will not be available' -ForegroundColor Yellow + } + else { + Write-Host " Retrieved 'Microsoft Defender for Cloud' SecureScore for $($getMgAscSecureScore.Count) Management Groups" + foreach ($entry in $getMgAscSecureScore) { + $script:htMgASCSecureScore.($entry.mgId) = @{} + if ($entry.secureScore -eq 404) { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = 'n/a' + } + else { + $script:htMgASCSecureScore.($entry.mgId).SecureScore = $entry.secureScore + } + } + } + } + + $end = Get-Date + Write-Host "Getting Microsoft Defender for Cloud Secure Score for Management Groups duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" +} +function getOrphanedResources { + $start = Get-Date + Write-Host 'Getting orphaned/unused resources (ARG)' + + $queries = [System.Collections.ArrayList]@() + $intent = 'cost savings - stopped but not deallocated VM' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.compute/virtualmachines' + query = "Resources | where type =~ 'microsoft.compute/virtualmachines' and properties.extended.instanceView.powerState.code =~ 'PowerState/stopped' | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'clean up' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.resources/subscriptions/resourceGroups' + query = "ResourceContainers | where type =~ 'microsoft.resources/subscriptions/resourceGroups' | extend rgAndSub = strcat(resourceGroup, '--', subscriptionId) | join kind=leftouter (Resources | extend rgAndSub = strcat(resourceGroup, '--', subscriptionId) | summarize count() by rgAndSub) on rgAndSub | where isnull(count_) | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'misconfiguration' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/networkSecurityGroups' + query = "Resources | where type =~ 'microsoft.network/networkSecurityGroups' and isnull(properties.networkInterfaces) and isnull(properties.subnets) | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'misconfiguration' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/routeTables' + query = "resources | where type =~ 'microsoft.network/routeTables' | where isnull(properties.subnets) | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'misconfiguration' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/networkInterfaces' + query = "Resources | where type =~ 'microsoft.network/networkInterfaces' | where isnull(properties.privateEndpoint) | where isnull(properties.privateLinkService) | where properties.hostedWorkloads == '[]' | where properties !has 'virtualmachine' | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'cost savings' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.compute/disks' + query = "Resources | where type =~ 'microsoft.compute/disks' | extend diskState = tostring(properties.diskState) | where managedBy == '' | where not(name endswith '-ASRReplica' or name startswith 'ms-asr-' or name startswith 'asrseeddisk-') | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'cost savings' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/publicIpAddresses' + query = "Resources | where type =~ 'microsoft.network/publicIpAddresses' | where properties.ipConfiguration == '' and properties.natGateway == '' | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'misconfiguration' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.compute/availabilitySets' + query = "Resources | where type =~ 'microsoft.compute/availabilitySets' | where properties.virtualMachines == '[]' | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'misconfiguration' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/loadBalancers' + query = "Resources | where type =~ 'microsoft.network/loadBalancers' | where properties.backendAddressPools == '[]' | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $intent = 'cost savings' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.network/applicationGateways' + query = "resources | where type =~ 'Microsoft.Network/applicationGateways' | extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools | project type, subscriptionId, Resource=id, Intent='$intent' | join (resources | where type =~ 'Microsoft.Network/applicationGateways' | mvexpand backendPools = properties.backendAddressPools | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName = backendPools.properties.backendAddressPools.name | extend Resource = id | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by Resource) on Resource | project-away Resource1 | where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount)) | order by Resource asc" + intent = $intent + }) + + $intent = 'cost savings' + $null = $queries.Add([PSCustomObject]@{ + queryName = 'microsoft.web/serverfarms' + query = "Resources | where type =~ 'microsoft.web/serverfarms' | where properties.numberOfSites == 0 | project type, subscriptionId, Resource=id, Intent='$intent'" + intent = $intent + }) + + $queries | ForEach-Object -Parallel { + $queryDetail = $_ + $arrayOrphanedResources = $using:arrayOrphanedResources + $subsToProcessInCustomDataCollection = $using:subsToProcessInCustomDataCollection + $azAPICallConf = $using:azAPICallConf + + #Batching: https://docs.microsoft.com/en-us/azure/governance/resource-graph/troubleshoot/general#toomanysubscription + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 1000 + $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" + $method = 'POST' + foreach ($batch in $subscriptionsBatch) { + Write-Host " Getting orphaned $($queryDetail.queryName) for $($batch.Group.subscriptionId.Count) Subscriptions" + $subscriptions = '"{0}"' -f ($batch.Group.subscriptionId -join '","') + $body = @" +{ + "query": "$($queryDetail.query)", + "subscriptions": [$($subscriptions)], + "options": { + "`$top": 1000 + } +} +"@ + + $res = (AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -listenOn 'Content' -currentTask "Getting orphaned $($queryDetail.queryName)") + #Write-Host '$res.count:' $res.count + if ($res.count -gt 0) { + foreach ($resource in $res) { + $null = $script:arrayOrphanedResources.Add($resource) + } + } + Write-Host " $($res.count) orphaned $($queryDetail.queryName) found" + } + } -ThrottleLimit ($queries.Count) + + if ($arrayOrphanedResources.Count -gt 0) { + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + $allConsumptionDataGroupedByTypeAndCurrency = $allConsumptionData | Group-Object -Property ResourceType, Currency + $orphanedResourcesResourceTypesCostRelevant = ($queries.where({ $_.intent -like 'cost savings*' })).queryName + + $htC = @{} + foreach ($consumptionResourceTypeAndCurrency in $allConsumptionDataGroupedByTypeAndCurrency) { + $consumptionResourceTypeAndCurrencySplitted = $consumptionResourceTypeAndCurrency.Name.split(', ') + if ($consumptionResourceTypeAndCurrencySplitted[0] -in $orphanedResourcesResourceTypesCostRelevant ) { + foreach ($entry in $consumptionResourceTypeAndCurrency.Group) { + if (-not $htC.($entry.resourceId)) { + $htC.($entry.resourceId) = @{} + $htC.($entry.resourceId).cost = $entry.PreTaxCost + $htC.($entry.resourceId).currency = $entry.Currency + } + else { + $htC.($entry.resourceId).cost = $htC.($entry.resourceId).cost + $entry.PreTaxCost + } + } + } + } + + $costrelevantOrphanedResourcesGroupedByType = ($arrayOrphanedResources | Group-Object -Property intent).where({ $_.name -like 'cost savings*' }).group | Group-Object -Property type + $nonCostrelevantOrphanedResourcesGroupedByType = ($arrayOrphanedResources | Group-Object -Property intent).where({ $_.name -notlike 'cost savings*' }).group | Group-Object -Property type + $script:arrayOrphanedResources = [System.Collections.ArrayList]@() + + foreach ($costrelevantOrphanedResourceType in $costrelevantOrphanedResourcesGroupedByType) { + foreach ($resource in $costrelevantOrphanedResourceType.Group) { + if ($htC.($resource.Resource)) { + $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ + Type = $costrelevantOrphanedResourceType.Name + Resource = $resource.Resource + SubscriptionId = $resource.subscriptionId + Intent = $resource.Intent + Cost = $htC.($resource.Resource).cost + Currency = $htC.($resource.Resource).currency + }) + } + else { + $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ + Type = $costrelevantOrphanedResourceType.Name + Resource = $resource.Resource + SubscriptionId = $resource.subscriptionId + Intent = $resource.Intent + Cost = '' + Currency = '' + }) + } + } + } + + foreach ($nonCostrelevantOrphanedResourceType in $nonCostrelevantOrphanedResourcesGroupedByType) { + Write-Host "Processing $($nonCostrelevantOrphanedResourceType.Name)" + foreach ($resource in $nonCostrelevantOrphanedResourceType.Group) { + $null = $script:arrayOrphanedResources.Add([PSCustomObject]@{ + Type = $nonCostrelevantOrphanedResourceType.Name + Resource = $resource.Resource + SubscriptionId = $resource.subscriptionId + Intent = $resource.Intent + Cost = '' + Currency = '' + }) + } + } + } + + Write-Host " Found $($arrayOrphanedResources.Count) orphaned/unused Resources" + if (-not $NoCsvExport) { + Write-Host " Exporting OrphanedResources CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv'" + $arrayOrphanedResources | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + else { + Write-Host ' No orphaned/unused Resources found' + } + + $end = Get-Date + Write-Host "Getting orphaned/unused resources (ARG) processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" +} +function getPIMEligible { + $start = Get-Date + + $currentTask = 'Get PIM onboarded Subscriptions and Management Groups' + Write-Host $currentTask + $uriExt = "&`$expand=parent&`$filter=(type eq 'subscription' or type eq 'managementgroup')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/resources?`$select=id,displayName,type,externalId" + $uriExt + $res = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -currentTask $currentTask + if ($res.Count -gt 0) { + + $scopesToIterate = [System.Collections.ArrayList]@() + if (-not $PIMEligibilityIgnoreScope) { + if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + foreach ($entry in $res) { + if ($entry.type -eq 'managementGroup') { + if ($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain -contains ($entry.externalId -replace '.*/') -or $htManagementGroupsMgPath.($entry.externalId -replace '.*/').path -contains $ManagementGroupId) { + $null = $scopesToIterate.Add($entry) + } + } + if ($entry.type -eq 'subscription') { + if ($htSubscriptionsMgPath.($entry.externalId -replace '.*/').ParentNameChain -contains $ManagementGroupId) { + if ($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/')) { + Write-Host "excluding subscription $($entry.externalId -replace '.*/') (outOfScopeSubscription -> $($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/').outOfScopeReason)) (`$PIMEligibilityIgnoreScope=$PIMEligibilityIgnoreScope)" + } + else { + $null = $scopesToIterate.Add($entry) + } + } + } + } + } + else { + foreach ($entry in $res) { + if ($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/')) { + Write-Host "excluding subscription $($entry.externalId -replace '.*/') (outOfScopeSubscription -> $($htOutOfScopeSubscriptions.($entry.externalId -replace '.*/').outOfScopeReason)) (`$PIMEligibilityIgnoreScope=$PIMEligibilityIgnoreScope)" + } + else { + $null = $scopesToIterate.Add($entry) + } + } + } + } + else { + foreach ($entry in $res) { + $null = $scopesToIterate.Add($entry) + } + } + + $PIMOnboardedGrouped = $scopesToIterate | Group-Object -Property type + foreach ($entry in $PIMOnboardedGrouped) { + Write-Host " Found $($entry.Count) PIM onboarded $($entry.Name)s" + } + + $htPIMEligibleDirect = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} + $relevantSubscriptionIds = $subsToProcessInCustomDataCollection.subscriptionId + $scopesToIterate | ForEach-Object -Parallel { + $scope = $_ + $azAPICallConf = $using:azAPICallConf + $arrayPIMEligible = $using:arrayPIMEligible + $htPIMEligibleDirect = $using:htPIMEligibleDirect + if ($scope.type -eq 'managementgroup') { $htManagementGroupsMgPath = $using:htManagementGroupsMgPath } + if ($scope.type -eq 'subscription') { $htSubscriptionsMgPath = $using:htSubscriptionsMgPath } + $htPrincipals = $using:htPrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $htServicePrincipals = $using:htServicePrincipals + $relevantSubscriptionIds = $using:relevantSubscriptionIds + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + + $processThisScope = $true + if ($scope.type -eq 'subscription') { + if (($scope.externalId -replace '.*/') -notin $relevantSubscriptionIds) { + Write-Host " Non relevant subscriptionId '$(($scope.externalId -replace '.*/'))' /skipping this subscription as it is not contained in the 'Relevant Subscriptions' collection (needs investigation)" -ForegroundColor DarkRed + $processThisScope = $false + } + } + + if ($processThisScope -eq $true) { + $currentTask = "Get Eligible assignments for Scope $($scope.type): $($scope.externalId -replace '.*/')" + $extUri = "?`$expand=linkedEligibleRoleAssignment,subject,roleDefinition(`$expand=resource)&`$count=true&`$filter=(roleDefinition/resource/id eq '$($scope.id)')+and+(assignmentState eq 'Eligible')&`$top=100" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/roleAssignments" + $extUri + $resx = AzAPICall -AzAPICallConfiguration $azapicallConf -currentTask $currentTask -uri $uri + + if ($resx.Count -gt 0) { + + $users = $resx.where({ $_.subject.type -eq 'user' }) + if ($users.Count -gt 0) { + ResolveObjectIds -objectIds $users.subject.id -showActivity + } + + foreach ($entry in $resx) { + $scopeId = $scope.externalId -replace '.*/' + if ($scope.type -eq 'managementgroup') { + $ScopeType = 'MG' + $ManagementGroupId = $scopeId + $SubscriptionId = '' + $SubscriptionDisplayName = '' + if ($htManagementGroupsMgPath.($scopeId)) { + $MgDetails = $htManagementGroupsMgPath.($scopeId) + $ManagementGroupDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + } + else { + $ManagementGroupDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + + if ($entry.memberType -eq 'direct') { + $script:htPIMEligibleDirect.($entry.id) = @{} + $script:htPIMEligibleDirect.($entry.id).clear = $scopeId + if ($scopeId -eq $ManagementGroupDisplayName) { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($scopeId) [Level $($MgLevel)]" + } + else { + $script:htPIMEligibleDirect.($entry.id).enriched = "$($ManagementGroupDisplayName) ($($scopeId)) [Level $($MgLevel)]" + } + } + } + if ($scope.type -eq 'subscription') { + $ScopeType = 'Sub' + #$ManagementGroupId = '' + $SubscriptionId = $scopeId + if ($htSubscriptionsMgPath.($scopeId)) { + $MgDetails = $htSubscriptionsMgPath.($scopeId) + $SubscriptionDisplayName = $MgDetails.DisplayName + $ScopeDisplayName = $MgDetails.DisplayName + $MgPath = $MgDetails.path + $MgLevel = $MgDetails.level + $ManagementGroupId = $MgDetails.Parent + $ManagementGroupDisplayName = $MgDetails.ParentName + } + else { + $SubscriptionDisplayName = 'notAccessible' + $ScopeDisplayName = 'notAccessible' + $MgPath = 'notAccessible' + $MgLevel = 'notAccessible' + } + #$ManagementGroupDisplayName = '' + + } + + if ($entry.subject.type -eq 'user') { + if ($htPrincipals.($entry.subject.id)) { + $userDetail = $htPrincipals.($entry.subject.id) + $principalType = "$($userDetail.type) $($userDetail.userType)" + } + else { + $principalType = $entry.subject.type + } + } + else { + $principalType = $entry.subject.type + } + + $roleType = 'undefined' + if ($entry.roleDefinition.type -eq 'BuiltInRole') { $roleType = 'Builtin' } + if ($entry.roleDefinition.type -eq 'CustomRole') { $roleType = 'Custom' } + + $null = $script:arrayPIMEligible.Add([PSCustomObject]@{ + ScopeType = $ScopeType + ScopeId = $scopeId + ScopeDisplayName = $ScopeDisplayName + ManagementGroupId = $ManagementGroupId + ManagementGroupDisplayName = $ManagementGroupDisplayName + SubscriptionId = $SubscriptionId + SubscriptionDisplayName = $SubscriptionDisplayName + MgPath = $MgPath + MgLevel = $MgLevel + RoleId = $entry.roleDefinition.externalId + RoleIdGuid = $entry.roleDefinition.externalId -replace '.*/' + RoleType = $roleType + RoleName = $entry.roleDefinition.displayName + IdentityObjectId = $entry.subject.id + IdentityType = $principalType + IdentityDisplayName = $entry.subject.displayName + IdentityPrincipalName = $entry.subject.principalName + PIMId = $entry.id + PIMInheritance = $entry.memberType + PIMInheritedFromClear = '' + PIMInheritedFrom = '' + PIMStartDateTime = $entry.startDateTime + PIMEndDateTime = $entry.endDateTime + }) + } + } + } + + } -ThrottleLimit $ThrottleLimit + + foreach ($entry in $arrayPIMEligible) { + if ($entry.PIMInheritance -eq 'inherited') { + $entry.PIMInheritedFromClear = $htPIMEligibleDirect.($entry.PIMId).clear + $entry.PIMInheritedFrom = $htPIMEligibleDirect.($entry.PIMId).enriched + } + } + + $script:arrayPIMEligibleGrouped = $arrayPIMEligible | Group-Object -Property ScopeType + foreach ($entry in $arrayPIMEligibleGrouped) { + Write-Host " Found $($entry.Count) PIM Eligible assignments for $($entry.Name)s" + } + } + + $end = Get-Date + Write-Host "Getting PIM Eligible assignments processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" +} +function getPolicyHash { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $json + ) + return [System.BitConverter]::ToString([System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($json))) +} +function getPolicyRemediation { + $currentTask = 'Getting NonCompliant (dine/modify)' + Write-Host $currentTask + #ref: https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" + $method = 'POST' + + if ($ManagementGroupsOnly) { + $queryNonCompliant = @' + policyresources + | where type == 'microsoft.policyinsights/policystates' and properties.policyAssignmentScope startswith '/providers/Microsoft.Management/managementGroups/' and (properties.policyDefinitionAction =~ 'deployifnotexists' or properties.policyDefinitionAction =~ 'modify') and properties.complianceState =~ 'NonCompliant' + | summarize count() by assignmentScope = tostring(properties.policyAssignmentScope), assignmentName = tostring(properties.policyAssignmentName), assignmentId = tostring(properties.policyAssignmentId), definitionName = tostring(properties.policyDefinitionName), definitionId = tostring(properties.policyDefinitionId), policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId), effect = tostring(properties.policyDefinitionAction) + | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect +'@ + } + else { + $queryNonCompliant = @' + policyresources + | where (properties.policyDefinitionAction =~ 'deployifnotexists' or properties.policyDefinitionAction =~ 'modify') and properties.complianceState =~ 'NonCompliant' + | summarize count() by assignmentScope = tostring(properties.policyAssignmentScope), assignmentName = tostring(properties.policyAssignmentName), assignmentId = tostring(properties.policyAssignmentId), definitionName = tostring(properties.policyDefinitionName), definitionId = tostring(properties.policyDefinitionId), policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId), effect = tostring(properties.policyDefinitionAction) + | sort by count_, assignmentId, definitionId, policyDefinitionReferenceId, effect +'@ + } + + + $body = @" + { + "query": "$($queryNonCompliant)", + "managementGroups":[ + "$($ManagementGroupId)" + ], + "options": { + "`$top": 1000 + } + } +"@ + + $getNonCompliant = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -body $body -listenOn 'Content' + $script:arrayRemediatable = [System.Collections.ArrayList]@() + Write-Host " Found $($getNonCompliant.Count) remediatable Policy definitions" + if ($getNonCompliant.Count -gt 0) { + Write-Host ' Enriching remediatable assignments with displayNames' + foreach ($nonCompliant in $getNonCompliant) { + + if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower())) { + if ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { + $policyAssignmentPolicyOrPolicySet = 'policySetDefinition' + $policySetDefinitionId = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId + $policySetDefinitionDisplayName = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).DisplayName + $policySetDefinitionName = $policySetDefinitionId -replace '.*/' + $policySetDefinitionType = $htCacheDefinitionsPolicySet.($policySetDefinitionId.ToLower()).Type + } + elseif ($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { + $policyAssignmentPolicyOrPolicySet = 'policyDefinition' + $policySetDefinitionId = 'n/a' + $policySetDefinitionDisplayName = 'n/a' + $policySetDefinitionName = 'n/a' + $policySetDefinitionType = 'n/a' + } + else { + throw "unexpected .policyDefinitionId: $($htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties)" + } + + switch ($nonCompliant.assignmentId) { + { $_ -like '/subscriptions/*' } { + $policyAssignmentScopeType = 'Sub' + } + { $_ -like '/subscriptions/*/resourcegroups/*' } { + $policyAssignmentScopeType = 'RG' + } + { $_ -like '/providers/Microsoft.Management/managementGroups/*' } { + $policyAssignmentScopeType = 'MG' + } + default { + $policyAssignmentScopeType = 'notDetected' + } + } + + $null = $script:arrayRemediatable.Add([PSCustomObject]@{ + policyAssignmentScopeType = $policyAssignmentScopeType + policyAssignmentScope = $nonCompliant.assignmentScope + policyAssignmentId = $nonCompliant.assignmentId + policyAssignmentName = $nonCompliant.assignmentName + policyAssignmentDisplayName = $htCacheAssignmentsPolicy.($nonCompliant.assignmentId.toLower()).assignment.properties.displayName + policyAssignmentPolicyOrPolicySet = $policyAssignmentPolicyOrPolicySet + effect = $nonCompliant.effect + policyDefinitionId = $nonCompliant.definitionId + policyDefinitionName = $nonCompliant.definitionName + policyDefinitionDisplayName = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Json.properties.displayName + policyDefinitionType = $htCacheDefinitionsPolicy.($nonCompliant.definitionId.toLower()).Type + policySetPolicyDefinitionReferenceId = $nonCompliant.policyDefinitionReferenceId + policySetDefinitionId = $policySetDefinitionId + policySetDefinitionName = $policySetDefinitionName + policySetDefinitionDisplayName = $policySetDefinitionDisplayName + policySetDefinitionType = $policySetDefinitionType + nonCompliantResourcesCount = $nonCompliant.count_ + }) + } + else { + Write-Host " skipping `$htCacheAssignmentsPolicy.($($nonCompliant.assignmentId)) potentially an assignment on an out-of-scope subscription" + } + } + } +} +function getResourceDiagnosticsCapability { + Write-Host 'Checking Resource Types Diagnostics capability (1st party only)' + $startResourceDiagnosticsCheck = Get-Date + if (($resourcesAll).count -gt 0) { + + $startGroupResourceIdsByType = Get-Date + $script:resourceTypesUnique = ($resourcesIdsAll | Group-Object -Property type) + $endGroupResourceIdsByType = Get-Date + Write-Host " GroupResourceIdsByType processing duration: $((New-TimeSpan -Start $startGroupResourceIdsByType -End $endGroupResourceIdsByType).TotalSeconds) seconds)" + $resourceTypesUniqueCount = ($resourceTypesUnique | Measure-Object).count + Write-Host " $($resourceTypesUniqueCount) unique Resource Types" + $script:resourceTypesSummarizedArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + + $script:resourceTypesDiagnosticsArray = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $microsoftResourceTypes = $resourceTypesUnique.where({ $_.Name.StartsWith('microsoft') }) + if ($microsoftResourceTypes.Count -gt 0) { + $microsoftResourceTypes | ForEach-Object -Parallel { + $resourceTypesUniqueGroup = $_ + $resourcetype = $resourceTypesUniqueGroup.Name + #region UsingVARs + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $ExcludedResourceTypesDiagnosticsCapable = $using:ExcludedResourceTypesDiagnosticsCapable + $resourceTypesDiagnosticsArray = $using:resourceTypesDiagnosticsArray + $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource + $resourceTypesSummarizedArray = $using:resourceTypesSummarizedArray + #endregion UsingVARs + + $skipThisResourceType = $false + if (($ExcludedResourceTypesDiagnosticsCapable).Count -gt 0) { + foreach ($excludedResourceType in $ExcludedResourceTypesDiagnosticsCapable) { + if ($excludedResourceType -eq $resourcetype) { + $skipThisResourceType = $true + } + } + } + + if ($skipThisResourceType -eq $false) { + $resourceCount = $resourceTypesUniqueGroup.Count + + #thx @Jim Britt (Microsoft) https://github.com/JimGBritt/AzurePolicy/tree/master/AzureMonitor/Scripts Create-AzDiagPolicy.ps1 + $responseJSON = '' + $logCategories = @() + $metrics = $false + $logs = $false + + $resourceAvailability = ($resourceCount - 1) + $counterTryForResourceType = 0 + do { + $counterTryForResourceType++ + if ($resourceCount -gt 1) { + $resourceId = $resourceTypesUniqueGroup.Group.Id[$resourceAvailability] + } + else { + $resourceId = $resourceTypesUniqueGroup.Group.Id + } + + $resourceAvailability = $resourceAvailability - 1 + if ($resourceId -like '*+*') { + Write-Host "resourceId '$resourceId' contains bad character '+'; skipping resourceId" + $responseJSON = 'skipResource' + } + else { + $currentTask = "Checking if ResourceType '$resourceType' is capable for Resource Diagnostics using $counterTryForResourceType ResourceId: '$($resourceId)'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/$($resourceId)/providers/microsoft.insights/diagnosticSettingsCategories?api-version=2021-05-01-preview" + $method = 'GET' + $responseJSON = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri ([uri]::EscapeUriString($uri)) -method $method -currentTask $currentTask + } + + if ($responseJSON -ne 'skipResource') { + if ($responseJSON -eq 'ResourceTypeOrResourceProviderNotSupported') { + Write-Host " ResourceTypeOrResourceProviderNotSupported | The resource type '$($resourcetype)' does not support diagnostic settings." + + } + else { + Write-Host " ResourceTypeSupported | The resource type '$($resourcetype)' supports diagnostic settings." + } + } + else { + Write-Host "resId '$resourceId' skipped" + } + } + until ($resourceAvailability -lt 0 -or $responseJSON -ne 'skipResource') + + if ($resourceAvailability -lt 0 -and $responseJSON -eq 'skipResource') { + Write-Host "tried for all available resourceIds ($($resourceCount)) for resourceType $resourceType, but seems all resourceIds needed to be skipped" + $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ + ResourceType = $resourcetype + Metrics = "n/a - $responseJSON" + Logs = "n/a - $responseJSON" + LogCategories = 'n/a' + ResourceCount = $resourceCount + }) + } + else { + if ($responseJSON) { + foreach ($response in $responseJSON) { + if ($response.properties.categoryType -eq 'Metrics') { + $metrics = $true + } + if ($response.properties.categoryType -eq 'Logs') { + $logs = $true + $logCategories += $response.name + } + } + } + + $null = $script:resourceTypesDiagnosticsArray.Add([PSCustomObject]@{ + ResourceType = $resourcetype + Metrics = $metrics + Logs = $logs + LogCategories = $logCategories + ResourceCount = $resourceCount + }) + } + } + else { + Write-Host "Skipping ResourceType $($resourcetype) as per parameter '-ExcludedResourceTypesDiagnosticsCapable'" + } + } -ThrottleLimit $ThrottleLimit + } + else { + Write-Host ' No 1st party Resource Types at all' + } + + } + else { + Write-Host ' No Resources at all' + } + $endResourceDiagnosticsCheck = Get-Date + Write-Host "Checking Resource Types Diagnostics capability duration: $((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalMinutes) minutes ($((New-TimeSpan -Start $startResourceDiagnosticsCheck -End $endResourceDiagnosticsCheck).TotalSeconds) seconds)" +} +function getSubscriptions { + $startGetSubscriptions = Get-Date + $currentTask = 'Getting all Subscriptions' + Write-Host "$currentTask" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions?api-version=2020-01-01" + $method = 'GET' + $requestAllSubscriptionsAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + $script:htAllSubscriptionsFromAPI = @{} + $script:htSubscriptionsFromOtherTenants = @{} + + Write-Host " $($requestAllSubscriptionsAPI.Count) Subscriptions returned" + foreach ($subscription in $requestAllSubscriptionsAPI) { + + if ($subscription.tenantId -ne $azAPICallConf['checkcontext'].tenant.id) { + Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - Azure Governance Visualizer: excluding this Subscripion" -ForegroundColor DarkRed + $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId) = @{} + $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId).subDetails = $subscription + } + else { + $script:htAllSubscriptionsFromAPI.($subscription.subscriptionId) = @{} + $script:htAllSubscriptionsFromAPI.($subscription.subscriptionId).subDetails = $subscription + } + } + Write-Host " $($htAllSubscriptionsFromAPI.Keys.Count) Subscriptions relevant" + + $endGetSubscriptions = Get-Date + Write-Host "Getting all Subscriptions duration: $((New-TimeSpan -Start $startGetSubscriptions -End $endGetSubscriptions).TotalSeconds) seconds" +} +function getTenantDetails { + $currentTask = 'Get Tenant details' + Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/tenants?api-version=2020-01-01" + $method = 'GET' + $tenantDetailsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if (($tenantDetailsResult).count -gt 0) { + $tenantDetails = $tenantDetailsResult | Where-Object { $_.tenantId -eq ($azAPICallConf['checkContext']).Tenant.Id } + if ($tenantDetails.displayName) { + $script:tenantDisplayName = $tenantDetails.displayName + Write-Host " Tenant DisplayName: $tenantDisplayName" + } + else { + Write-Host ' Tenant DisplayName: could not be retrieved' + } + + if ($tenantDetails.defaultDomain) { + $script:tenantDefaultDomain = $tenantDetails.defaultDomain + } + } + else { + Write-Host ' something unexpected' + } +} +function handleCloudEnvironment { + Write-Host "Environment: $($azAPICallConf['checkContext'].Environment.Name)" + if ($DoAzureConsumption) { + if ($azAPICallConf['checkContext'].Environment.Name -eq 'AzureChinaCloud') { + Write-Host 'Azure Billing not supported in AzureChinaCloud, skipping Consumption..' + $script:DoAzureConsumption = $false + } + } +} +function NamingValidation($toCheck) { + $checks = @(':', '/', '\', '<', '>', '|', '"') + $array = @() + foreach ($check in $checks) { + if ($toCheck -like "*$($check)*") { + $array += $check + } + } + if ($toCheck -match '\*') { + $array += '*' + } + if ($toCheck -match '\?') { + $array += '?' + } + return $array +} +function prepareData { + Write-Host 'Preparing Data' + $startPreparingArrays = Get-Date + $script:optimizedTableForPathQuery = ($newTable | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, subscriptionId -Unique + $hlperOptimizedTableForPathQuery = $optimizedTableForPathQuery.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) } ) + $script:optimizedTableForPathQueryMgAndSub = ($hlperOptimizedTableForPathQuery | Select-Object -Property level, mg*, subscription*) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName, subscriptionId, subscription -Unique + $script:optimizedTableForPathQueryMg = ($optimizedTableForPathQuery.where( { [String]::IsNullOrEmpty($_.SubscriptionId) } ) | Select-Object -Property level, mgid, mgName, mgparentid, mgparentName) | Sort-Object -Property level, mgid, mgname, mgparentId, mgparentName -Unique + $script:optimizedTableForPathQuerySub = ($hlperOptimizedTableForPathQuery | Select-Object -Property subscription*) | Sort-Object -Property subscriptionId -Unique + + foreach ($entry in $optimizedTableForPathQuery) { + $script:htMgDetails.($entry.mgId) = @{} + $mgSubs = $optimizedTableForPathQueryMgAndSub.where( { $_.mgId -eq $entry.mgId } ) + $script:htMgDetails.($entry.mgId).subscriptionsCount = $mgSubs.Count + $script:htMgDetails.($entry.mgId).subscriptions = $mgSubs + $script:htMgDetails.($entry.mgId).details = $entry + $mgChildren = ($optimizedTableForPathQueryMg.where( { $_.mgParentId -eq $entry.mgId } )).MgId + $script:htMgDetails.($entry.mgId).mgChildren = $mgChildren + $script:htMgDetails.($entry.mgId).mgChildrenCount = $mgChildren.Count + } + + foreach ($entry in $optimizedTableForPathQueryMgAndSub) { + $script:htSubDetails.($entry.SubscriptionId) = @{} + $script:htSubDetails.($entry.SubscriptionId).details = $optimizedTableForPathQueryMgAndSub.where( { $_.SubscriptionId -eq $entry.SubscriptionId } ) + } + + $script:parentMgBaseQuery = ($optimizedTableForPathQueryMg.where( { $_.MgParentId -eq $getMgParentId } )) + $script:parentMgNamex = $parentMgBaseQuery.mgParentName | Get-Unique + $script:parentMgIdx = $parentMgBaseQuery.mgParentId | Get-Unique + + $endPreparingArrays = Get-Date + Write-Host "Preparing Arrays duration: $((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalMinutes) minutes ($((New-TimeSpan -Start $startPreparingArrays -End $endPreparingArrays).TotalSeconds) seconds)" +} +function processAADGroups { + if ($NoPIMEligibility) { + Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment exists)' + } + else { + Write-Host 'Resolving AAD Groups (for which a RBAC Role assignment or PIM Eligibility exists)' + } + + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (before Resolving AAD Groups)" + $startAADGroupsResolveMembers = Get-Date + + $roleAssignmentsforGroups = ($roleAssignmentsUniqueById.where( { $_.RoleAssignmentIdentityObjectType -eq 'Group' } ) | Select-Object -Property RoleAssignmentIdentityObjectId, RoleAssignmentIdentityDisplayname) | Sort-Object -Property RoleAssignmentIdentityObjectId -Unique + $optimizedTableForAADGroupsQuery = [System.Collections.ArrayList]@() + if ($roleAssignmentsforGroups.Count -gt 0) { + foreach ($roleAssignmentforGroups in $roleAssignmentsforGroups) { + $null = $optimizedTableForAADGroupsQuery.Add($roleAssignmentforGroups) + } + } + + $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count + Write-Host " $aadGroupsCount Groups from RoleAssignments" + + if (-not $NoPIMEligibility) { + $PIMEligibleGroups = $arrayPIMEligible.where({ $_.IdentityType -eq 'Group' }) | Select-Object IdentityObjectId, IdentityDisplayName | Sort-Object -Property IdentityObjectId -Unique + $cntPIMEligibleGroupsTotal = 0 + $cntPIMEligibleGroupsNotCoveredFromRoleAssignments = 0 + foreach ($PIMEligibleGroup in $PIMEligibleGroups) { + $cntPIMEligibleGroupsTotal++ + if ($optimizedTableForAADGroupsQuery.RoleAssignmentIdentityObjectId -notcontains $PIMEligibleGroup.IdentityObjectId) { + $cntPIMEligibleGroupsNotCoveredFromRoleAssignments++ + $null = $optimizedTableForAADGroupsQuery.Add([PSCustomObject]@{ + RoleAssignmentIdentityObjectId = $PIMEligibleGroup.IdentityObjectId + RoleAssignmentIdentityDisplayname = $PIMEligibleGroup.IdentityDisplayName + }) + } + } + Write-Host " $cntPIMEligibleGroupsTotal Groups from PIM Eligibility; $cntPIMEligibleGroupsNotCoveredFromRoleAssignments Groups added ($($cntPIMEligibleGroupsTotal - $cntPIMEligibleGroupsNotCoveredFromRoleAssignments) already covered in RoleAssignments)" + $aadGroupsCount = ($optimizedTableForAADGroupsQuery).Count + Write-Host " $aadGroupsCount Groups from RoleAssignments and PIM Eligibility" + } + + if ($aadGroupsCount -gt 0) { + + switch ($aadGroupsCount) { + { $_ -gt 0 } { $indicator = 1 } + { $_ -gt 10 } { $indicator = 5 } + { $_ -gt 50 } { $indicator = 10 } + { $_ -gt 100 } { $indicator = 20 } + { $_ -gt 250 } { $indicator = 25 } + { $_ -gt 500 } { $indicator = 50 } + { $_ -gt 1000 } { $indicator = 100 } + { $_ -gt 10000 } { $indicator = 250 } + } + + Write-Host " processing $($aadGroupsCount) AAD Groups (indicating progress in steps of $indicator)" + + $optimizedTableForAADGroupsQuery | ForEach-Object -Parallel { + $aadGroupIdWithRoleAssignment = $_ + #region UsingVARs + #fromOtherFunctions + $AADGroupMembersLimit = $using:AADGroupMembersLimit + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $htAADGroupsDetails = $using:htAADGroupsDetails + $arrayGroupRoleAssignmentsOnServicePrincipals = $using:arrayGroupRoleAssignmentsOnServicePrincipals + $arrayGroupRequestResourceNotFound = $using:arrayGroupRequestResourceNotFound + $arrayProgressedAADGroups = $using:arrayProgressedAADGroups + $htAADGroupsExeedingMemberLimit = $using:htAADGroupsExeedingMemberLimit + $indicator = $using:indicator + $htUserTypesGuest = $using:htUserTypesGuest + $htServicePrincipals = $using:htServicePrincipals + #other + $function:getGroupmembers = $using:funcGetGroupmembers + #endregion UsingVARs + + $rndom = Get-Random -Minimum 10 -Maximum 750 + Start-Sleep -Millisecond $rndom + + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/groups/$($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)/transitiveMembers/`$count" + $method = 'GET' + $aadGroupMembersCount = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask "getGroupMembersCountTransitive $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)" -listenOn 'Content' -consistencyLevel 'eventual' + + if ($aadGroupMembersCount -eq 'Request_ResourceNotFound') { + $null = $script:arrayGroupRequestResourceNotFound.Add([PSCustomObject]@{ + groupId = $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId + }) + } + else { + if ($aadGroupMembersCount -gt $AADGroupMembersLimit) { + Write-Host " Group exceeding limit ($($AADGroupMembersLimit)); memberCount: $aadGroupMembersCount; Group: $($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname) ($($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId)); Members will not be resolved adjust the limit using parameter -AADGroupMembersLimit" + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) = @{} + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersAllCount = $aadGroupMembersCount + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersUsersCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersGroupsCount = 'n/a' + $script:htAADGroupsDetails.($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId).MembersServicePrincipalsCount = 'n/a' + } + else { + getGroupmembers -aadGroupId $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId -aadGroupDisplayName $aadGroupIdWithRoleAssignment.RoleAssignmentIdentityDisplayname + } + } + + $null = $script:arrayProgressedAADGroups.Add($aadGroupIdWithRoleAssignment.RoleAssignmentIdentityObjectId) + $processedAADGroupsCount = $null + $processedAADGroupsCount = ($arrayProgressedAADGroups).Count + if ($processedAADGroupsCount) { + if ($processedAADGroupsCount % $indicator -eq 0) { + Write-Host " $processedAADGroupsCount AAD Groups processed" + } + } + } -ThrottleLimit ($ThrottleLimit * 2) + } + else { + Write-Host " processing $($aadGroupsCount) AAD Groups" + } + + $arrayGroupRequestResourceNotFoundCount = ($arrayGroupRequestResourceNotFound).Count + if ($arrayGroupRequestResourceNotFoundCount -gt 0) { + Write-Host "$arrayGroupRequestResourceNotFoundCount Groups could not be checked for Memberships" + } + + Write-Host " processed $($arrayProgressedAADGroups.Count) AAD Groups" + $endAADGroupsResolveMembers = Get-Date + Write-Host "Resolving AAD Groups duration: $((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADGroupsResolveMembers -End $endAADGroupsResolveMembers).TotalSeconds) seconds)" + Write-Host " Users known as Guest count: $($htUserTypesGuest.Keys.Count) (after Resolving AAD Groups)" +} +function processALZPolicyVersionChecker { + $start = Get-Date + Write-Host "Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data" + $ALZRepositoryURI = 'https://github.com/Azure/Enterprise-Scale.git' + $workingPath = Get-Location + Write-Host " Working directory is '$($workingPath)'" + $ALZFolderName = "ALZ_$(Get-Date -Format $FileTimeStampFormat)" + $ALZPath = "$($OutputPath)/$($ALZFolderName)" + + if (-not (Test-Path -LiteralPath "$($ALZPath)")) { + Write-Host " Creating temporary directory '$($ALZPath)'" + $null = mkdir $ALZPath + } + else { + Write-Host " Unexpected: The path '$($ALZPath)' already exists" + throw + } + + Write-Host " Switching to temporary directory '$($ALZPath)'" + Set-Location $ALZPath + $ALZCloneSuccess = $false + + try { + Write-Host " Try cloning '$($ALZRepositoryURI)'" + git clone $ALZRepositoryURI + if (-not (Test-Path -LiteralPath "$($ALZPath)/Enterprise-Scale" -PathType Container)) { + $ALZCloneSuccess = $false + Write-Host " Cloning '$($ALZRepositoryURI)' failed" + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true + Write-Host " Switching back to working directory '$($workingPath)'" + Set-Location $workingPath + } + else { + Write-Host " Cloning '$($ALZRepositoryURI)' succeeded" + $ALZCloneSuccess = $true + } + } + catch { + $_ + Write-Host " Cloning '$($ALZRepositoryURI)' failed" + Write-Host " Setting switch parameter '-NoALZPolicyVersionChecker' to true" + $script:NoALZPolicyVersionChecker = $true + $script:azAPICallConf['htParameters'].NoALZPolicyVersionChecker = $true + Write-Host " Switching back to working directory '$($workingPath)'" + Set-Location $workingPath + } + + if ($ALZCloneSuccess) { + Write-Host " Switching to directory '$($ALZPath)/Enterprise-Scale'" + Set-Location "$($ALZPath)/Enterprise-Scale" + + $allESLZPolicies = @{} + $allESLZPolicySets = @{} + $allESLZPolicyHashes = @{} + $allESLZPolicySetHashes = @{} + + #Write-Host " Processing ALZ Data Policy definitions" + $gitHist = (git log --format="%ai`t%H`t%an`t%ae`t%s" -- ./eslzArm/managementGroupTemplates/policyDefinitions/dataPolicies.json) | ConvertFrom-Csv -Delimiter "`t" -Header ('Date', 'CommitId', 'Author', 'Email', 'Subject') + $commitCount = 0 + $processDataPolicies = $true + foreach ($commit in $gitHist | Sort-Object -Property Date) { + if ($processDataPolicies) { + if ($commit.CommitId -eq '3476914f9ba9a8f3f641a25497dfb24a4efa1017') { + $processDataPolicies = $false + continue + } + #Write-Host "processing commit (dataPolicies) $($commit.CommitId)" + $commitCount++ + $jsonRaw = git show "$($commit.CommitId):eslzArm/managementGroupTemplates/policyDefinitions/dataPolicies.json" + + $jsonESLZPolicies = $jsonRaw | ConvertFrom-Json + if (($jsonESLZPolicies.variables.policies.policyDefinitions).Count -eq 0) { + } + else { + $eslzPolicies = $jsonESLZPolicies.variables.policies.policyDefinitions + foreach ($policyDefinition in $eslzPolicies) { + $policyJsonConv = ($policyDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' + $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json + $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 + $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) + $stringHash = [System.BitConverter]::ToString($hash) + + if (-not $allESLZPolicies.($policyJsonRebuild.name)) { + $allESLZPolicies.($policyJsonRebuild.name) = @{} + $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name + $allESLZPolicies.($policyJsonRebuild.name).metadataSource = '' + + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + } + else { + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + + if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + } + } + + #hsh + if (-not $allESLZPolicyHashes.($stringHash)) { + $allESLZPolicyHashes.($stringHash) = @{} + $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name + $allESLZPolicyHashes.($stringHash).metadataSource = '' + + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + } + else { + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { + $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name + } + } + } + } + } + } + + #Write-Host " Processing ALZ Policy and Set definitions" + $gitHist = (git log --format="%ai`t%H`t%an`t%ae`t%s" -- ./eslzArm/managementGroupTemplates/policyDefinitions/policies.json) | ConvertFrom-Csv -Delimiter "`t" -Header ('Date', 'CommitId', 'Author', 'Email', 'Subject') + $commitCount = 0 + $doNewALZPolicyReadingApproach = $false + foreach ($commit in $gitHist | Sort-Object -Property Date) { + + if ($commit.CommitId -eq '3476914f9ba9a8f3f641a25497dfb24a4efa1017') { + $doNewALZPolicyReadingApproach = $true + } + #Write-Host "processing commit $($commit.CommitId) - doNewALZPolicyReadingApproach: $doNewALZPolicyReadingApproach" + $commitCount++ + + $jsonRaw = git show "$($commit.CommitId):eslzArm/managementGroupTemplates/policyDefinitions/policies.json" + + if ($doNewALZPolicyReadingApproach) { + $jsonESLZPolicies = $jsonRaw -replace '\[\[', '[' | ConvertFrom-Json + [regex]$extractVariableName = "(?<=\[variables\(')[^']+" + $refsPolicyDefinitionsAll = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.All).Value + $refsPolicyDefinitionsAzureCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureCloud).Value + $refsPolicyDefinitionsAzureChinaCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureChinaCloud).Value + $refsPolicyDefinitionsAzureUSGovernment = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicyDefinitions.AzureUSGovernment).Value + $refsPolicySetDefinitionsAll = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.All).Value + $refsPolicySetDefinitionsAzureCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureCloud).Value + $refsPolicySetDefinitionsAzureChinaCloud = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureChinaCloud).Value + $refsPolicySetDefinitionsAzureUSGovernment = $extractVariableName.Matches($jsonESLZPolicies.variables.loadPolicySetDefinitions.AzureUSGovernment).Value + $listPolicyDefinitionsAzureCloud = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureCloud + $listPolicyDefinitionsAzureChinaCloud = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureChinaCloud + $listPolicyDefinitionsAzureUSGovernment = $refsPolicyDefinitionsAll + $refsPolicyDefinitionsAzureUSGovernment + $listPolicySetDefinitionsAzureCloud = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureCloud + $listPolicySetDefinitionsAzureChinaCloud = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureChinaCloud + $listPolicySetDefinitionsAzureUSGovernment = $refsPolicySetDefinitionsAll + $refsPolicySetDefinitionsAzureUSGovernment + $policyDefinitionsAzureCloud = $listPolicyDefinitionsAzureCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) + $policyDefinitionsAzureChinaCloud = $listPolicyDefinitionsAzureChinaCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) + $policyDefinitionsAzureUSGovernment = $listPolicyDefinitionsAzureUSGovernment.ForEach({ $jsonESLZPolicies.variables.$_ }) + $policySetDefinitionsAzureCloud = $listPolicySetDefinitionsAzureCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) + $policySetDefinitionsAzureChinaCloud = $listPolicySetDefinitionsAzureChinaCloud.ForEach({ $jsonESLZPolicies.variables.$_ }) + $policySetDefinitionsAzureUSGovernment = $listPolicySetDefinitionsAzureUSGovernment.ForEach({ $jsonESLZPolicies.variables.$_ }) + + switch ($azAPICallConf['checkContext'].Environment.Name) { + 'Azurecloud' { + $policyDefinitionsData = $policyDefinitionsAzureCloud + $policySetDefinitionsData = $policySetDefinitionsAzureCloud + } + 'AzureChinaCloud' { + $policyDefinitionsData = $policyDefinitionsAzureChinaCloud + $policySetDefinitionsData = $policySetDefinitionsAzureChinaCloud + } + 'AzureUSGovernment' { + $policyDefinitionsData = $policyDefinitionsAzureUSGovernment + $policySetDefinitionsData = $policySetDefinitionsAzureUSGovernment + } + } + + foreach ($policyDefinition in $policyDefinitionsData) { + + $policyJsonRebuild = $policyDefinition | ConvertFrom-Json + $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 + $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) + $stringHash = [System.BitConverter]::ToString($hash) + + if (-not $allESLZPolicies.($policyJsonRebuild.name)) { + $allESLZPolicies.($policyJsonRebuild.name) = @{} + $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name + $allESLZPolicies.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + } + $allESLZPolicies.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + } + } + + #hsh + if (-not $allESLZPolicyHashes.($stringHash)) { + $allESLZPolicyHashes.($stringHash) = @{} + $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name + $allESLZPolicyHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicyHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicyHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + } + $allESLZPolicyHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { + $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name + } + } + } + + foreach ($policySetDefinition in $policySetDefinitionsData) { + + $policyJsonRebuild = $policySetDefinition | ConvertFrom-Json + $policyJsonParameters = $policyJsonRebuild.properties.parameters | ConvertTo-Json -Depth 99 + $policyJsonPolicyDefinitions = $policyJsonRebuild.properties.policyDefinitions | ConvertTo-Json -Depth 99 + $hashParameters = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonParameters)) + $stringHashParameters = [System.BitConverter]::ToString($hashParameters) + $hashPolicyDefinitions = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonPolicyDefinitions)) + $stringHashPolicyDefinitions = [System.BitConverter]::ToString($hashPolicyDefinitions) + $stringHash = "$($stringHashParameters)_$($stringHashPolicyDefinitions)" + + if (-not $allESLZPolicySets.($policyJsonRebuild.name)) { + $allESLZPolicySets.($policyJsonRebuild.name) = @{} + $allESLZPolicySets.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + $allESLZPolicySets.($policyJsonRebuild.name).name = $policyJsonRebuild.name + $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' + } + $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($allESLZPolicySets.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicySets.($policyJsonRebuild.name).$stringHash) { + $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + } + } + + #hsh + if (-not $allESLZPolicySetHashes.($stringHash)) { + $allESLZPolicySetHashes.($stringHash) = @{} + $allESLZPolicySetHashes.($stringHash).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicySetHashes.($stringHash).name = $policyJsonRebuild.name + $allESLZPolicySetHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySetHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicySetHashes.($stringHash).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySetHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicySetHashes.($stringHash).status = 'obsolete' + } + $allESLZPolicySetHashes.($stringHash).metadataSource = $policyJsonRebuild.properties.metadata.source + if ($allESLZPolicySetHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name)) { + $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name + } + } + } + } + else { + $jsonESLZPolicies = $jsonRaw | ConvertFrom-Json + if (($jsonESLZPolicies.variables.policies.policyDefinitions).Count -eq 0) { + } + else { + + $eslzPolicies = $jsonESLZPolicies.variables.policies.policyDefinitions + foreach ($policyDefinition in $eslzPolicies) { + $policyJsonConv = ($policyDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' + $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json + $policyJsonRule = $policyJsonRebuild.properties.policyRule | ConvertTo-Json -Depth 99 + $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) + $stringHash = [System.BitConverter]::ToString($hash) + + if (-not $allESLZPolicies.($policyJsonRebuild.name)) { + $allESLZPolicies.($policyJsonRebuild.name) = @{} + $allESLZPolicies.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + $allESLZPolicies.($policyJsonRebuild.name).name = $policyJsonRebuild.name + $allESLZPolicies.($policyJsonRebuild.name).metadataSource = '' + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicies.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicies.($policyJsonRebuild.name).status = 'obsolete' + } + if ($allESLZPolicies.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicies.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicies.($policyJsonRebuild.name).$stringHash) { + $allESLZPolicies.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + } + } + + #hsh + if (-not $allESLZPolicyHashes.($stringHash)) { + $allESLZPolicyHashes.($stringHash) = @{} + $allESLZPolicyHashes.($stringHash).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicyHashes.($stringHash).name = $policyJsonRebuild.name + $allESLZPolicyHashes.($stringHash).metadataSource = '' + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicyHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicyHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicyHashes.($stringHash).status = 'obsolete' + } + if ($allESLZPolicyHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicyHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name)) { + $allESLZPolicyHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name + } + } + } + + $eslzPolicySets = $jsonESLZPolicies.variables.initiatives.policySetDefinitions + foreach ($policySetDefinition in $eslzPolicySets) { + + $policyJsonConv = ($policySetDefinition | ConvertTo-Json -Depth 99) -replace '\[\[', '[' + $policyJsonRebuild = $policyJsonConv | ConvertFrom-Json + $policyJsonParameters = $policyJsonRebuild.properties.parameters | ConvertTo-Json -Depth 99 + $policyJsonPolicyDefinitions = $policyJsonRebuild.properties.policyDefinitions | ConvertTo-Json -Depth 99 + $hashParameters = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonParameters)) + $stringHashParameters = [System.BitConverter]::ToString($hashParameters) + $hashPolicyDefinitions = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonPolicyDefinitions)) + $stringHashPolicyDefinitions = [System.BitConverter]::ToString($hashPolicyDefinitions) + $stringHash = "$($stringHashParameters)_$($stringHashPolicyDefinitions)" + + if (-not $allESLZPolicySets.($policyJsonRebuild.name)) { + $allESLZPolicySets.($policyJsonRebuild.name) = @{} + $allESLZPolicySets.($policyJsonRebuild.name).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + $allESLZPolicySets.($policyJsonRebuild.name).name = $policyJsonRebuild.name + $allESLZPolicySets.($policyJsonRebuild.name).metadataSource = '' + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'prod' + } + else { + $allESLZPolicySets.($policyJsonRebuild.name).status = 'obsolete' + } + if ($allESLZPolicySets.($policyJsonRebuild.name).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicySets.($policyJsonRebuild.name).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicySets.($policyJsonRebuild.name).$stringHash) { + $allESLZPolicySets.($policyJsonRebuild.name).$stringHash = $policyJsonRebuild.properties.metadata.version + } + } + + #hsh + if (-not $allESLZPolicySetHashes.($stringHash)) { + $allESLZPolicySetHashes.($stringHash) = @{} + $allESLZPolicySetHashes.($stringHash).version = [System.Collections.ArrayList]@() + $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + $allESLZPolicySetHashes.($stringHash).name = $policyJsonRebuild.name + $allESLZPolicySetHashes.($stringHash).metadataSource = '' + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySetHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicySetHashes.($stringHash).status = 'obsolete' + } + } + else { + if ($commitCount -eq $gitHist.Count) { + $allESLZPolicySetHashes.($stringHash).status = 'prod' + } + else { + $allESLZPolicySetHashes.($stringHash).status = 'obsolete' + } + if ($allESLZPolicySetHashes.($stringHash).version -notcontains $policyJsonRebuild.properties.metadata.version) { + $null = $allESLZPolicySetHashes.($stringHash).version.Add($policyJsonRebuild.properties.metadata.version) + } + if (-not $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name)) { + $allESLZPolicySetHashes.($stringHash).($policyJsonRebuild.name) = $policyJsonRebuild.name + } + } + } + } + } + } + + + Write-Host " $($allESLZPolicies.Keys.Count) Azure Landing Zones (ALZ) Policy definitions ($($allESLZPolicies.Values.where({$_.status -eq 'Prod'}).Count) productive)" + Write-Host " $($allESLZPolicySets.Keys.Count) Azure Landing Zones (ALZ) PolicySet definitions ($($allESLZPolicySets.Values.where({$_.status -eq 'Prod'}).Count) productive)" + + $arrayObsoleteALZPolicies = @( + 'Deny-PublicEndpoint-Aks', + 'Deny-PublicEndpoint-CosmosDB', + 'Deny-PublicEndpoint-KeyVault', + 'Deny-PublicEndpoint-MySQL', + 'Deny-PublicEndpoint-PostgreSql', + 'Deny-PublicEndpoint-Sql', + 'Deny-PublicEndpoint-Storage', + 'Deploy-ASC-Standard', + 'Deploy-Diagnostics-ActivityLog', + 'Deploy-Diagnostics-AKS', + 'Deploy-Diagnostics-Batch', + 'Deploy-Diagnostics-DataLakeStore', + 'Deploy-Diagnostics-EventHub', + 'Deploy-Diagnostics-KeyVault', + 'Deploy-Diagnostics-LogicAppsWF', + 'Deploy-Diagnostics-PublicIP', + 'Deploy-Diagnostics-RecoveryVault', + 'Deploy-Diagnostics-SearchServices', + 'Deploy-Diagnostics-ServiceBus', + 'Deploy-Diagnostics-SQLDBs', + 'Deploy-Diagnostics-StreamAnalytics', + 'Deploy-DNSZoneGroup-For-Blob-PrivateEndpoint', + 'Deploy-DNSZoneGroup-For-File-PrivateEndpoint', + 'Deploy-DNSZoneGroup-For-KeyVault-PrivateEndpoint', + 'Deploy-DNSZoneGroup-For-Queue-PrivateEndpoint', + 'Deploy-DNSZoneGroup-For-Sql-PrivateEndpoint', + 'Deploy-DNSZoneGroup-For-Table-PrivateEndpoint', + 'Deploy-HUB', + 'Deploy-LA-Config', + 'Deploy-Log-Analytics', + 'Deploy-vHUB', + 'Deploy-vNet', + 'Deploy-vWAN' + ) + foreach ($obsoleteALZPolicy in $arrayObsoleteALZPolicies) { + if (-not $alzPolicies.($obsoleteALZPolicy)) { + $script:alzPolicies.($obsoleteALZPolicy) = @{} + $script:alzPolicies.($obsoleteALZPolicy).latestVersion = '' + $script:alzPolicies.($obsoleteALZPolicy).status = 'obsolete' + $script:alzPolicies.($obsoleteALZPolicy).policyName = $obsoleteALZPolicy + $script:alzPolicies.($obsoleteALZPolicy).metadataSource = '' + } + } + + foreach ($entry in $allESLZPolicies.keys | Sort-Object) { + $thisOne = $allESLZPolicies.($entry) + $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] + $script:alzPolicies.($entry) = @{} + $script:alzPolicies.($entry).latestVersion = $latestVersion + $script:alzPolicies.($entry).status = $thisOne.status + $script:alzPolicies.($entry).policyName = $thisOne.name + $script:alzPolicies.($entry).metadataSource = $thisOne.name + } + + foreach ($entry in $allESLZPolicyHashes.keys | Sort-Object) { + $thisOne = $allESLZPolicyHashes.($entry) + $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] + $script:alzPolicyHashes.($entry) = @{} + $script:alzPolicyHashes.($entry).latestVersion = $latestVersion + $script:alzPolicyHashes.($entry).status = $thisOne.status + $script:alzPolicyHashes.($entry).policyName = $thisOne.name + $script:alzPolicyHashes.($entry).metadataSource = $thisOne.metadataSource + } + + $script:alzPolicySets.'Deploy-Diag-LogAnalytics' = @{} + $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.latestVersion = '1.0.0' + $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.status = 'obsolete' + $script:alzPolicySets.'Deploy-Diag-LogAnalytics'.policySetName = 'Deploy-Diag-LogAnalytics' + foreach ($entry in $allESLZPolicySets.keys | Sort-Object) { + $thisOne = $allESLZPolicySets.($entry) + $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] + $script:alzPolicySets.($entry) = @{} + $script:alzPolicySets.($entry).latestVersion = $latestVersion + $script:alzPolicySets.($entry).status = $thisOne.status + $script:alzPolicySets.($entry).policySetName = $thisOne.name + $script:alzPolicySets.($entry).metadataSource = $thisOne.metadataSource + } + + foreach ($entry in $allESLZPolicySetHashes.keys | Sort-Object) { + $thisOne = $allESLZPolicySetHashes.($entry) + $latestVersion = ([array]($thisOne.version | Sort-Object -Descending))[0] + $script:alzPolicySetHashes.($entry) = @{} + $script:alzPolicySetHashes.($entry).latestVersion = $latestVersion + $script:alzPolicySetHashes.($entry).status = $thisOne.status + $script:alzPolicySetHashes.($entry).policySetName = $thisOne.name + $script:alzPolicySetHashes.($entry).metadataSource = $thisOne.metadataSource + } + + Write-Host " Switching back to working directory '$($workingPath)'" + Set-Location $workingPath + + Write-Host " Removing temporary directory '$($ALZPath)'" + Remove-Item -Recurse -Force $ALZPath + } + + $end = Get-Date + Write-Host " Processing 'Azure Landing Zones (ALZ) Policy Version Checker' base data duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" +} +function processApplications { + Write-Host 'Processing Service Principals - Applications' + $script:servicePrincipalsOfTypeApplication = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'Application' -and $htServicePrincipals.($_).appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Subscription.TenantId } ) + if ($azAPICallConf['htParameters'].userType -eq 'Guest') { + #checking if Guest has enough permissions + $app4Test = $htServicePrincipals.($servicePrincipalsOfTypeApplication[0]) + $currentTask = "getApp Test $($app4Test.appId)" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($app4Test.appId)'" + $method = 'GET' + $testGetApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + if ($testGetApplication -eq 'skipApplications') { + $skipApplications = $true + Write-Host ' Guest account does not have enough permissions, skipping Applications (Secrets & Certificates)' + } + } + if (-not $skipApplications) { + $startSPApp = Get-Date + $currentDateUTC = (Get-Date).ToUniversalTime() + $script:arrayApplicationRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $servicePrincipalsOfTypeApplication | ForEach-Object -Parallel { + + #region UsingVARs + $currentDateUTC = $using:currentDateUTC + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $arrayApplicationRequestResourceNotFound = $using:arrayApplicationRequestResourceNotFound + $htAppDetails = $using:htAppDetails + $htServicePrincipals = $using:htServicePrincipals + #endregion UsingVARs + + $sp = $htServicePrincipals.($_) + + $currentTask = "getApp $($sp.appId)" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/applications?`$filter=appId eq '$($sp.appId)'" + $method = 'GET' + $getApplication = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($getApplication -eq 'Request_ResourceNotFound') { + $null = $script:arrayApplicationRequestResourceNotFound.Add([PSCustomObject]@{ + appId = $sp.appId + }) + } + else { + if (($getApplication).Count -eq 0) { + Write-Host "$($sp.appId) no data returned / seems non existent?" + } + else { + $script:htAppDetails.($sp.id) = @{} + $script:htAppDetails.($sp.id).servicePrincipalType = $sp.servicePrincipalType + $script:htAppDetails.($sp.id).spGraphDetails = $sp + $script:htAppDetails.($sp.id).appGraphDetails = $getApplication + + $appPasswordCredentialsCount = ($getApplication.passwordCredentials).count + if ($appPasswordCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appPasswordCredentialsCount = $appPasswordCredentialsCount + $appPasswordCredentialsExpiredCount = 0 + $appPasswordCredentialsGracePeriodExpiryCount = 0 + $appPasswordCredentialsExpiryOKCount = 0 + $appPasswordCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appPasswordCredential in $getApplication.passwordCredentials) { + $passwordExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appPasswordCredential.endDateTime).TotalDays + if ($passwordExpiryTotalDays -lt 0) { + $appPasswordCredentialsExpiredCount++ + } + elseif ($passwordExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appPasswordCredentialsGracePeriodExpiryCount++ + } + else { + if ($passwordExpiryTotalDays -gt 730) { + $appPasswordCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appPasswordCredentialsExpiryOKCount++ + } + } + } + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiredCount = $appPasswordCredentialsExpiredCount + $script:htAppDetails.($sp.id).appPasswordCredentialsGracePeriodExpiryCount = $appPasswordCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKCount = $appPasswordCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appPasswordCredentialsExpiryOKMoreThan2YearsCount = $appPasswordCredentialsExpiryOKMoreThan2YearsCount + } + + $appKeyCredentialsCount = ($getApplication.keyCredentials).count + if ($appKeyCredentialsCount -gt 0) { + $script:htAppDetails.($sp.id).appKeyCredentialsCount = $appKeyCredentialsCount + $appKeyCredentialsExpiredCount = 0 + $appKeyCredentialsGracePeriodExpiryCount = 0 + $appKeyCredentialsExpiryOKCount = 0 + $appKeyCredentialsExpiryOKMoreThan2YearsCount = 0 + foreach ($appKeyCredential in $getApplication.keyCredentials) { + $keyCredentialExpiryTotalDays = (New-TimeSpan -Start $currentDateUTC -End $appKeyCredential.endDateTime).TotalDays + if ($keyCredentialExpiryTotalDays -lt 0) { + $appKeyCredentialsExpiredCount++ + } + elseif ($keyCredentialExpiryTotalDays -lt $AADServicePrincipalExpiryWarningDays) { + $appKeyCredentialsGracePeriodExpiryCount++ + } + else { + if ($keyCredentialExpiryTotalDays -gt 730) { + $appKeyCredentialsExpiryOKMoreThan2YearsCount++ + } + else { + $appKeyCredentialsExpiryOKCount++ + } + } + } + $script:htAppDetails.($sp.id).appKeyCredentialsExpiredCount = $appKeyCredentialsExpiredCount + $script:htAppDetails.($sp.id).appKeyCredentialsGracePeriodExpiryCount = $appKeyCredentialsGracePeriodExpiryCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKCount = $appKeyCredentialsExpiryOKCount + $script:htAppDetails.($sp.id).appKeyCredentialsExpiryOKMoreThan2YearsCount = $appKeyCredentialsExpiryOKMoreThan2YearsCount + } + } + } + + } -ThrottleLimit ($ThrottleLimit * 2) + + $endSPApp = Get-Date + Write-Host "Processing Service Principals - Applications duration: $((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPApp -End $endSPApp).TotalSeconds) seconds)" + } +} +function processDataCollection { + [CmdletBinding()]Param( + [string]$mgId + ) + + Write-Host ' CustomDataCollection ManagementGroups' + $startMgLoop = Get-Date + + $allManagementGroupsFromEntitiesChildOfRequestedMg = $arrayEntitiesFromAPI.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and ($_.Name -eq $mgId -or $_.properties.parentNameChain -contains $mgId) }) + $allManagementGroupsFromEntitiesChildOfRequestedMgCount = ($allManagementGroupsFromEntitiesChildOfRequestedMg).Count + Write-Host " $allManagementGroupsFromEntitiesChildOfRequestedMgCount Management Groups: $(($allManagementGroupsFromEntitiesChildOfRequestedMg.Name | Sort-Object) -join ', ')" + $mgBatch = ($allManagementGroupsFromEntitiesChildOfRequestedMg | Group-Object -Property { ($_.properties.parentNameChain).Count }) | Sort-Object -Property Name + Write-Host " $(($mgBatch | Measure-Object).Count) batches of Management Groups to process:" + + $btchCnt = 0 + foreach ($btch in $mgBatch) { + $btchCnt++ + $listOfMGs = @() + foreach ($btchMg in $btch.Group | Sort-Object -Property name) { + if ($btchMg.name -eq $btchMg.Properties.displayName) { + $listOfMGs += $btchMg.name + } + else { + $listOfMGs += "$($btchMg.name) ($($btchMg.Properties.displayName))" + } + } + Write-Host " Batch#$($btchCnt) - $($listOfMGs.Count) Management Groups: $($listOfMGs -join ', ')" + } + + foreach ($batchLevel in $mgBatch) { + Write-Host " Processing Management Groups L$($batchLevel.Name) ($($batchLevel.Count) Management Groups)" + + showMemoryUsage + + $batchLevel.Group | ForEach-Object -Parallel { + $mgdetail = $_ + #region UsingVARs + #Parameters MG&Sub related + $CsvDelimiter = $using:CsvDelimiter + $CsvDelimiterOpposite = $using:CsvDelimiterOpposite + $ManagementGroupId = $using:ManagementGroupId + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $newTable = $using:newTable + $customDataCollectionDuration = $using:customDataCollectionDuration + $htSubscriptionTagList = $using:htSubscriptionTagList + $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource + $htAllTagList = $using:htAllTagList + $htSubscriptionTags = $using:htSubscriptionTags + $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy + $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet + $htCacheDefinitionsRole = $using:htCacheDefinitionsRole + $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint + $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $htCachePolicyComplianceMG = $using:htCachePolicyComplianceMG + $htCachePolicyComplianceResponseTooLargeMG = $using:htCachePolicyComplianceResponseTooLargeMG + $htCacheAssignmentsRole = $using:htCacheAssignmentsRole + $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources + $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint + $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy + $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions + $htManagementGroupsMgPath = $using:htManagementGroupsMgPath + $LimitPOLICYPolicyDefinitionsScopedManagementGroup = $using:LimitPOLICYPolicyDefinitionsScopedManagementGroup + $LimitPOLICYPolicySetDefinitionsScopedManagementGroup = $using:LimitPOLICYPolicySetDefinitionsScopedManagementGroup + $LimitPOLICYPolicyAssignmentsManagementGroup = $using:LimitPOLICYPolicyAssignmentsManagementGroup + $LimitPOLICYPolicySetAssignmentsManagementGroup = $using:LimitPOLICYPolicySetAssignmentsManagementGroup + $LimitRBACRoleAssignmentsManagementGroup = $using:LimitRBACRoleAssignmentsManagementGroup + $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI + $allManagementGroupsFromEntitiesChildOfRequestedMgCount = $using:allManagementGroupsFromEntitiesChildOfRequestedMgCount + $arrayDataCollectionProgressMg = $using:arrayDataCollectionProgressMg + $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub + $htMgAtScopePolicyAssignments = $using:htMgAtScopePolicyAssignments + $htMgAtScopePoliciesScoped = $using:htMgAtScopePoliciesScoped + $htMgAtScopeRoleAssignments = $using:htMgAtScopeRoleAssignments + $htMgASCSecureScore = $using:htMgASCSecureScore + $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention + $htNamingValidation = $using:htNamingValidation + $htPrincipals = $using:htPrincipals + $htServicePrincipals = $using:htServicePrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM + $alzPolicies = $using:alzPolicies + $alzPolicySets = $using:alzPolicySets + $alzPolicyHashes = $using:alzPolicyHashes + $alzPolicySetHashes = $using:alzPolicySetHashes + $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances + $ValidPolicyEffects = $using:ValidPolicyEffects + #other + $function:addRowToTable = $using:funcAddRowToTable + $function:namingValidation = $using:funcNamingValidation + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + + $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore + $function:dataCollectionDiagnosticsMG = $using:funcDataCollectionDiagnosticsMG + $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates + $function:dataCollectionBluePrintDefinitionsMG = $using:funcDataCollectionBluePrintDefinitionsMG + $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions + $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions + $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions + $function:dataCollectionPolicyAssignmentsMG = $using:funcDataCollectionPolicyAssignmentsMG + $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions + $function:dataCollectionRoleAssignmentsMG = $using:funcDataCollectionRoleAssignmentsMG + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + + #endregion usingVARS + $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount + + $addRowToTableDone = $false + + $MgDetailThis = $htManagementGroupsMgPath.($mgdetail.Name) + $MgParentId = $MgDetailThis.Parent + $hierarchyLevel = $MgDetailThis.ParentNameChainCount + + if ($MgParentId -eq '__TenantRoot__') { + $MgParentId = 'TenantRoot' + $MgParentName = $MgParentId + } + else { + $MgParentName = $htManagementGroupsMgPath.($MgParentId).DisplayName + } + + $rndom = Get-Random -Minimum 10 -Maximum 750 + Start-Sleep -Millisecond $rndom + $startMgLoopThis = Get-Date + + if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { + + #namingValidation + if (-not [string]::IsNullOrEmpty($mgdetail.properties.displayName)) { + $namingValidationResult = NamingValidation -toCheck $mgdetail.properties.displayName + if ($namingValidationResult.Count -gt 0) { + $script:htNamingValidation.ManagementGroup.($mgdetail.Name) = @{} + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.ManagementGroup.($mgdetail.Name).name = $mgdetail.properties.displayName + } + } + + $targetMgOrSub = 'MG' + $baseParameters = @{ + scopeId = $mgdetail.Name + scopeDisplayName = $mgdetail.properties.displayName + } + + #ManagementGroupASCSecureScore + $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $mgdetail.Name + + $addRowToTableParameters = @{ + hierarchyLevel = $hierarchyLevel + mgParentId = $mgParentId + mgParentName = $mgParentName + mgAscSecureScoreResult = $mgAscSecureScoreResult + } + + #mg diag + DataCollectionDiagnosticsMG @baseParameters + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + #MGPolicyCompliance + DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub + } + + #MGBlueprintDefinitions + $functionReturn = DataCollectionBluePrintDefinitionsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #MGPolicyExemptions + DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub + + #MGPolicyDefinitions + $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' + + #MGPolicySetDefinitions + $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' + + if (-not $htMgAtScopePoliciesScoped.($mgdetail.Name)) { + $script:htMgAtScopePoliciesScoped.($mgdetail.Name) = @{} + $script:htMgAtScopePoliciesScoped.($mgdetail.Name).ScopedCount = $policyDefinitionsScopedCount + $policySetDefinitionsScopedCount + } + + $scopedPolicyCounts = @{ + policyDefinitionsScopedCount = $policyDefinitionsScopedCount + policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount + } + + #MgPolicyAssignments + $functionReturn = DataCollectionPolicyAssignmentsMG @baseParameters @addRowToTableParameters @scopedPolicyCounts + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #MGRoleDefinitions + DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + + #MGRoleAssignments + $functionReturn = DataCollectionRoleAssignmentsMG @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + if ($addRowToTableDone -ne $true) { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $mgdetail.properties.displayName ` + -mgId $mgdetail.Name ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult + } + } + else { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $mgdetail.properties.displayName ` + -mgId $mgdetail.Name ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult + } + + + $endMgLoopThis = Get-Date + $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ + Type = 'Mg' + Id = $mgdetail.Name + DurationSec = (New-TimeSpan -Start $startMgLoopThis -End $endMgLoopThis).TotalSeconds + }) + + $null = $script:arrayDataCollectionProgressMg.Add($mgdetail.Name) + $progressCount = ($arrayDataCollectionProgressMg).Count + Write-Host " $($progressCount)/$($allManagementGroupsFromEntitiesChildOfRequestedMgCount) Management Groups processed" + + } -ThrottleLimit $ThrottleLimit + } + + $endMgLoop = Get-Date + Write-Host " CustomDataCollection ManagementGroups processing duration: $((New-TimeSpan -Start $startMgLoop -End $endMgLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startMgLoop -End $endMgLoop).TotalSeconds) seconds)" + + apiCallTracking -stage 'CustomDataCollection ManagementGroups' -spacing ' ' + + #test + if ($builtInPolicyDefinitionsCount -ne ($($htCacheDefinitionsPolicy).Values.where({ $_.Type -eq 'BuiltIn' }).Count) -or $builtInPolicyDefinitionsCount -ne ((($htCacheDefinitionsPolicy).Values.where( { $_.Type -eq 'BuiltIn' } )).Count)) { + Write-Host "$builtInPolicyDefinitionsCount -ne $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count) OR $builtInPolicyDefinitionsCount -ne $((($htCacheDefinitionsPolicy).Values.where( {$_.Type -eq 'BuiltIn'} )).Count)" + Write-Host 'Listing all PolicyDefinitions:' + foreach ($tmpPolicyDefinitionId in ($($htCacheDefinitionsPolicy).Keys | Sort-Object)) { + Write-Host $tmpPolicyDefinitionId + } + } + + + #region SUBSCRIPTION + Write-Host ' CustomDataCollection Subscriptions' + + if ($outOfScopeSubscriptions.Count -gt 0) { + Write-Host " CustomDataCollection $($outOfScopeSubscriptions.Count) Subscriptions excluded" -ForegroundColor yellow + $outOfScopeSubscriptionsGroupedByOutOfScopeReason = $outOfScopeSubscriptions | Group-Object -Property outOfScopeReason + foreach ($exclusionreason in $outOfScopeSubscriptionsGroupedByOutOfScopeReason) { + Write-Host " $($exclusionreason.Count): $($exclusionreason.Name)" + } + } + + Write-Host " CustomDataCollection Subscriptions will process $subsToProcessInCustomDataCollectionCount of $childrenSubscriptionsCount" + + $startSubLoop = Get-Date + if ($subsToProcessInCustomDataCollectionCount -gt 0) { + + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 100 + if ($subsToProcessInCustomDataCollectionCount -gt 500) { + $batchSize = 200 + } + Write-Host " Subscriptions Batch size: $batchSize" + + $subscriptionsBatch = $subsToProcessInCustomDataCollection | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + $batchCnt = 0 + foreach ($batch in $subscriptionsBatch) { + $startBatch = Get-Date + $batchCnt++ + Write-Host " processing Batch #$batchCnt/$(($subscriptionsBatch | Measure-Object).Count) ($(($batch.Group | Measure-Object).Count) Subscriptions)" + showMemoryUsage + + $batch.Group | ForEach-Object -Parallel { + $startSubLoopThis = Get-Date + $childMgSubDetail = $_ + #region UsingVARs + #Parameters MG&Sub related + $CsvDelimiter = $using:CsvDelimiter + $CsvDelimiterOpposite = $using:CsvDelimiterOpposite + #Parameters Sub related + #fromOtherFunctions + $azAPICallConf = $using:azAPICallConf + $scriptPath = $using:ScriptPath + #Array&HTs + $newTable = $using:newTable + $storageAccounts = $using:storageAccounts + $resourcesAll = $using:resourcesAll + $resourcesIdsAll = $using:resourcesIdsAll + $resourceGroupsAll = $using:resourceGroupsAll + $customDataCollectionDuration = $using:customDataCollectionDuration + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htManagementGroupsMgPath = $using:htManagementGroupsMgPath + $htResourceProvidersAll = $using:htResourceProvidersAll + $arrayFeaturesAll = $using:arrayFeaturesAll + $htSubscriptionTagList = $using:htSubscriptionTagList + $htResourceTypesUniqueResource = $using:htResourceTypesUniqueResource + $htAllTagList = $using:htAllTagList + $htSubscriptionTags = $using:htSubscriptionTags + $htCacheDefinitionsPolicy = $using:htCacheDefinitionsPolicy + $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet + $htCacheDefinitionsRole = $using:htCacheDefinitionsRole + $htCacheDefinitionsBlueprint = $using:htCacheDefinitionsBlueprint + $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $htCachePolicyComplianceSUB = $using:htCachePolicyComplianceSUB + $htCachePolicyComplianceResponseTooLargeSUB = $using:htCachePolicyComplianceResponseTooLargeSUB + $htCacheAssignmentsRole = $using:htCacheAssignmentsRole + $htCacheAssignmentsRBACOnResourceGroupsAndResources = $using:htCacheAssignmentsRBACOnResourceGroupsAndResources + $htCacheAssignmentsBlueprint = $using:htCacheAssignmentsBlueprint + $htCacheAssignmentsPolicyOnResourceGroupsAndResources = $using:htCacheAssignmentsPolicyOnResourceGroupsAndResources + $htCacheAssignmentsPolicy = $using:htCacheAssignmentsPolicy + $htPolicyAssignmentExemptions = $using:htPolicyAssignmentExemptions + $htResourceLocks = $using:htResourceLocks + $LimitPOLICYPolicyDefinitionsScopedSubscription = $using:LimitPOLICYPolicyDefinitionsScopedSubscription + $LimitPOLICYPolicySetDefinitionsScopedSubscription = $using:LimitPOLICYPolicySetDefinitionsScopedSubscription + $LimitPOLICYPolicyAssignmentsSubscription = $using:LimitPOLICYPolicyAssignmentsSubscription + $LimitPOLICYPolicySetAssignmentsSubscription = $using:LimitPOLICYPolicySetAssignmentsSubscription + $childrenSubscriptionsCount = $using:childrenSubscriptionsCount + $subsToProcessInCustomDataCollectionCount = $using:subsToProcessInCustomDataCollectionCount + $arrayDataCollectionProgressSub = $using:arrayDataCollectionProgressSub + $arraySubResourcesAddArrayDuration = $using:arraySubResourcesAddArrayDuration + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $arrayEntitiesFromAPI = $using:arrayEntitiesFromAPI + $arrayDiagnosticSettingsMgSub = $using:arrayDiagnosticSettingsMgSub + $htMgASCSecureScore = $using:htMgASCSecureScore + $htRoleAssignmentsFromAPIInheritancePrevention = $using:htRoleAssignmentsFromAPIInheritancePrevention + $htNamingValidation = $using:htNamingValidation + $htPrincipals = $using:htPrincipals + $htServicePrincipals = $using:htServicePrincipals + $htUserTypesGuest = $using:htUserTypesGuest + $arrayDefenderPlans = $using:arrayDefenderPlans + $arrayDefenderPlansSubscriptionsSkipped = $using:arrayDefenderPlansSubscriptionsSkipped + $arrayUserAssignedIdentities4Resources = $using:arrayUserAssignedIdentities4Resources + $htSubscriptionsRoleAssignmentLimit = $using:htSubscriptionsRoleAssignmentLimit + $arrayPsRule = $using:arrayPsRule + $arrayPSRuleTracking = $using:arrayPSRuleTracking + $htClassicAdministrators = $using:htClassicAdministrators + $htRoleAssignmentsPIM = $using:htRoleAssignmentsPIM + $alzPolicies = $using:alzPolicies + $alzPolicySets = $using:alzPolicySets + $alzPolicyHashes = $using:alzPolicyHashes + $alzPolicySetHashes = $using:alzPolicySetHashes + $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances + $htDefenderEmailContacts = $using:htDefenderEmailContacts + $arrayVNets = $using:arrayVNets + $arrayPrivateEndPoints = $using:arrayPrivateEndPoints + $htResourceProvidersRef = $using:htResourceProvidersRef + $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties + $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + $arrayAdvisorScores = $using:arrayAdvisorScores + $ValidPolicyEffects = $using:ValidPolicyEffects + #$htResourcesWithProperties = $using:htResourcesWithProperties + #other + $function:addRowToTable = $using:funcAddRowToTable + $function:namingValidation = $using:funcNamingValidation + $function:resolveObjectIds = $using:funcResolveObjectIds + $function:testGuid = $using:funcTestGuid + $function:dataCollectionMGSecureScore = $using:funcDataCollectionMGSecureScore + $function:dataCollectionDefenderPlans = $using:funcDataCollectionDefenderPlans + $function:dataCollectionDiagnosticsSub = $using:funcDataCollectionDiagnosticsSub + $function:dataCollectionResources = $using:funcDataCollectionResources + $function:dataCollectionStorageAccounts = $using:funcDataCollectionStorageAccounts + $function:dataCollectionResourceGroups = $using:funcDataCollectionResourceGroups + $function:dataCollectionResourceProviders = $using:funcDataCollectionResourceProviders + $function:dataCollectionFeatures = $using:funcDataCollectionFeatures + $function:dataCollectionResourceLocks = $using:funcDataCollectionResourceLocks + $function:dataCollectionTags = $using:funcDataCollectionTags + $function:dataCollectionPolicyComplianceStates = $using:funcDataCollectionPolicyComplianceStates + $function:dataCollectionASCSecureScoreSub = $using:funcDataCollectionASCSecureScoreSub + $function:dataCollectionBluePrintDefinitionsSub = $using:funcDataCollectionBluePrintDefinitionsSub + $function:dataCollectionBluePrintAssignmentsSub = $using:funcDataCollectionBluePrintAssignmentsSub + $function:dataCollectionPolicyExemptions = $using:funcDataCollectionPolicyExemptions + $function:dataCollectionPolicyDefinitions = $using:funcDataCollectionPolicyDefinitions + $function:dataCollectionPolicySetDefinitions = $using:funcDataCollectionPolicySetDefinitions + $function:dataCollectionPolicyAssignmentsSub = $using:funcDataCollectionPolicyAssignmentsSub + $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions + $function:dataCollectionRoleAssignmentsSub = $using:funcDataCollectionRoleAssignmentsSub + $function:dataCollectionClassicAdministratorsSub = $using:funcDataCollectionClassicAdministratorsSub + $function:dataCollectionDefenderEmailContacts = $using:funcDataCollectionDefenderEmailContacts + $function:dataCollectionVNets = $using:funcDataCollectionVNets + $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints + $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + #endregion UsingVARs + + $addRowToTableDone = $false + + $childMgSubId = $childMgSubDetail.subscriptionId + $childMgSubDisplayName = $childMgSubDetail.subscriptionName + $hierarchyInfo = $htSubscriptionsMgPath.($childMgSubDetail.subscriptionId) + $hierarchyLevel = $hierarchyInfo.level + $childMgId = $hierarchyInfo.Parent + $childMgDisplayName = $hierarchyInfo.ParentName + $childMgMgPath = $hierarchyInfo.pathDelimited + $childMgParentNameChain = $hierarchyInfo.ParentNameChain + $childMgParentNameChainDelimited = $hierarchyInfo.ParentNameChainDelimited + $childMgParentInfo = $htManagementGroupsMgPath.($childMgId) + $childMgParentId = $childMgParentInfo.Parent + $childMgParentName = $htManagementGroupsMgPath.($childMgParentInfo.Parent).DisplayName + + #namingValidation + if (-not [string]::IsNullOrEmpty($childMgSubDisplayName)) { + $namingValidationResult = NamingValidation -toCheck $childMgSubDisplayName + if ($namingValidationResult.Count -gt 0) { + + $script:htNamingValidation.Subscription.($childMgSubId) = @{} + $script:htNamingValidation.Subscription.($childMgSubId).displayNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.Subscription.($childMgSubId).displayName = $childMgSubDisplayName + } + } + + if ($azAPICallConf['htParameters'].HierarchyMapOnly -eq $false) { + $currentSubscription = $htAllSubscriptionsFromAPI.($childMgSubId).subDetails + $subscriptionQuotaId = $currentSubscription.subscriptionPolicies.quotaId + $subscriptionState = $currentSubscription.state + + $targetMgOrSub = 'Sub' + $baseParameters = @{ + scopeId = $childMgSubId + scopeDisplayName = $childMgSubDisplayName + subscriptionQuotaId = $subscriptionQuotaId + } + + if (-not $azAPICallConf['htParameters'].ManagementGroupsOnly) { + #mgSecureScore + $mgAscSecureScoreResult = DataCollectionMGSecureScore -Id $childMgId + + #defenderPlans + $dataCollectionDefenderPlansParameters = @{ + ChildMgMgPath = $childMgMgPath + } + DataCollectionDefenderPlans @baseParameters @dataCollectionDefenderPlansParameters + + #defenderEmailContacts + DataCollectionDefenderEmailContacts @baseParameters + + #advisorScores + $dataCollectionAdvisorScoresParameters = @{ + ChildMgMgPath = $childMgMgPath + } + DataCollectionAdvisorScores @baseParameters @dataCollectionAdvisorScoresParameters + + if (-not $azAPICallConf['htParameters'].NoNetwork) { + #VNets + DataCollectionVNets @baseParameters + #PE + DataCollectionPrivateEndpoints @baseParameters + } + + #diagnostics + $dataCollectionDiagnosticsSubParameters = @{ + ChildMgMgPath = $childMgMgPath + ChildMgId = $childMgId + } + DataCollectionDiagnosticsSub @baseParameters @dataCollectionDiagnosticsSubParameters + + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + #resources + $dataCollectionStorageAccountsParameters = @{ + ChildMgMgPath = $childMgMgPath + ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited + } + DataCollectionStorageAccounts @baseParameters @dataCollectionStorageAccountsParameters + } + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #resources + $dataCollectionResourcesParameters = @{ + ChildMgMgPath = $childMgMgPath + ChildMgParentNameChainDelimited = $childMgParentNameChainDelimited + } + DataCollectionResources @baseParameters @dataCollectionResourcesParameters + } + + #resourceGroups + DataCollectionResourceGroups @baseParameters + + #resourceProviders + if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { + DataCollectionResourceProviders @baseParameters + } + + #features + DataCollectionFeatures @baseParameters -MgParentNameChain $childMgParentNameChain + + #resourceLocks + DataCollectionResourceLocks @baseParameters + + #tags + $subscriptionTagsReturn = DataCollectionTags @baseParameters + $subscriptionTags = $subscriptionTagsReturn.subscriptionTags + $subscriptionTagsCount = $subscriptionTagsReturn.subscriptionTagsCount + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + #SubscriptionPolicyCompliance + DataCollectionPolicyComplianceStates @baseParameters -TargetMgOrSub $targetMgOrSub + } + + #SubscriptionASCSecureScore + $subscriptionASCSecureScore = DataCollectionASCSecureScoreSub @baseParameters + + $addRowToTableParameters = @{ + hierarchyLevel = $hierarchyLevel + childMgDisplayName = $childMgDisplayName + childMgId = $childMgId + childMgParentId = $childMgParentId + childMgParentName = $childMgParentName + mgAscSecureScoreResult = $mgAscSecureScoreResult + #subscriptionQuotaId = $subscriptionQuotaId + subscriptionState = $subscriptionState + subscriptionASCSecureScore = $subscriptionASCSecureScore + subscriptionTags = $subscriptionTags + subscriptionTagsCount = $subscriptionTagsCount + } + + #SubscriptionBlueprintDefinitions + $functionReturn = DataCollectionBluePrintDefinitionsSub @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #SubscriptionBlueprintAssignments + $functionReturn = DataCollectionBluePrintAssignmentsSub @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #SubscriptionPolicyExemptions + DataCollectionPolicyExemptions @baseParameters -TargetMgOrSub $targetMgOrSub + + #SubscriptionPolicyDefinitions + $functionReturn = DataCollectionPolicyDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policyDefinitionsScopedCount = $functionReturn.'PolicyDefinitionsScopedCount' + + #SubscriptionPolicySets + $functionReturn = DataCollectionPolicySetDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + $policySetDefinitionsScopedCount = $functionReturn.'PolicySetDefinitionsScopedCount' + + $scopedPolicyCounts = @{ + policyDefinitionsScopedCount = $policyDefinitionsScopedCount + policySetDefinitionsScopedCount = $policySetDefinitionsScopedCount + } + + #SubscriptionPolicyAssignments + $functionReturn = DataCollectionPolicyAssignmentsSub @baseParameters @addRowToTableParameters @scopedPolicyCounts + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #SubscriptionRoleDefinitions + DataCollectionRoleDefinitions @baseParameters -TargetMgOrSub $targetMgOrSub + + #SubscriptionRoleAssignments + $functionReturn = DataCollectionRoleAssignmentsSub @baseParameters @addRowToTableParameters + if ($functionReturn.'addRowToTableDone') { + $addRowToTableDone = $true + } + + #SubscriptionClassicAdministrators + dataCollectionClassicAdministratorsSub @baseParameters -SubscriptionMgPath $childMgMgPath + } + + if ($addRowToTableDone -ne $true) { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $childMgSubDisplayName ` + -SubscriptionId $childMgSubId ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore + } + } + else { + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $childMgSubDisplayName ` + -SubscriptionId $childMgSubId ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore + } + $endSubLoopThis = Get-Date + $null = $script:customDataCollectionDuration.Add([PSCustomObject]@{ + Type = 'SUB' + Id = $childMgSubId + DurationSec = (New-TimeSpan -Start $startSubLoopThis -End $endSubLoopThis).TotalSeconds + }) + + $null = $script:arrayDataCollectionProgressSub.Add($childMgSubId) + $progressCount = ($arrayDataCollectionProgressSub).Count + Write-Host " $($progressCount)/$($subsToProcessInCustomDataCollectionCount) Subscriptions processed" + + } -ThrottleLimit $ThrottleLimit + + $endBatch = Get-Date + Write-Host " Batch #$batchCnt processing duration: $((New-TimeSpan -Start $startBatch -End $endBatch).TotalMinutes) minutes ($((New-TimeSpan -Start $startBatch -End $endBatch).TotalSeconds) seconds)" + } + + $endSubLoop = Get-Date + Write-Host " CustomDataCollection Subscriptions processing duration: $((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubLoop -End $endSubLoop).TotalSeconds) seconds)" + if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { + if ($arrayPSRuleTracking.Count -gt 0) { + $durationPSRuleTotalSeconds = (($arrayPSRuleTracking.duration | Measure-Object -Sum).Sum) + Write-Host " CustomDataCollection Subscriptions 'PSRule for Azure' processing duration (in sum): $($durationPSRuleTotalSeconds / 60) minutes ($($durationPSRuleTotalSeconds) seconds)" + } + } + #test + Write-Host " built-in PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count)" + Write-Host " custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'Custom'}).Count)" + Write-Host " all PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.Count)" + } + #endregion SUBSCRIPTION + + $durationDataMG = $customDataCollectionDuration.where( { $_.Type -eq 'MG' } ) + $durationDataSUB = $customDataCollectionDuration.where( { $_.Type -eq 'SUB' } ) + $durationMGAverageMaxMin = ($durationDataMG.DurationSec | Measure-Object -Average -Maximum -Minimum) + $durationSUBAverageMaxMin = ($durationDataSUB.DurationSec | Measure-Object -Average -Maximum -Minimum) + Write-Host "Collecting custom data for $($arrayEntitiesFromAPIManagementGroupsCount) ManagementGroups Avg/Max/Min duration in seconds: Average: $([math]::Round($durationMGAverageMaxMin.Average,4)); Maximum: $([math]::Round($durationMGAverageMaxMin.Maximum,4)); Minimum: $([math]::Round($durationMGAverageMaxMin.Minimum,4))" + Write-Host "Collecting custom data for $($arrayEntitiesFromAPISubscriptionsCount) Subscriptions Avg/Max/Min duration in seconds: Average: $([math]::Round($durationSUBAverageMaxMin.Average,4)); Maximum: $([math]::Round($durationSUBAverageMaxMin.Maximum,4)); Minimum: $([math]::Round($durationSUBAverageMaxMin.Minimum,4))" + + apiCallTracking -stage 'CustomDataCollection ManagementGroups and Subscriptions' -spacing ' ' + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + + $script:resourcesAllGroupedBySubcriptionId = $resourcesAll | Group-Object -Property subscriptionId + + $totaldurationSubResourcesAddArray = ($arraySubResourcesAddArrayDuration.DurationSec | Measure-Object -Sum).Sum + Write-Host "Collecting custom data total duration writing the subResourcesArray: $totaldurationSubResourcesAddArray seconds" + + if (-not $azAPICallConf['htParameters'].HierarchyMapOnly -and -not $azAPICallConf['htParameters'].ManagementGroupsOnly) { + if (-not $NoCsvExport) { + + #fluctuation + Write-Host 'Process Resource fluctuation' + $start = Get-Date + if (Test-Path -Filter "*$($ManagementGroupId)_ResourcesAll.csv" -LiteralPath "$($outputPath)") { + $startImportPrevious = Get-Date + $doResourceFluctuation = $true + + try { + $previous = Get-ChildItem -Path $outputPath -Filter "*$($ManagementGroupId)_ResourcesAll.csv" | Sort-Object -Descending -Property LastWriteTime | Select-Object -First 1 -ErrorAction Stop + $importPrevious = Import-Csv -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($previous.Name)" -Encoding utf8 -Delimiter $CsvDelimiter | Select-Object -ExpandProperty id + Write-Host " Import previous ($($previous.Name)) duration: $((New-TimeSpan -Start $startImportPrevious -End (Get-Date)).TotalSeconds) seconds" + } + catch { + Write-Host " FAILED: importing previous CSV '$($outputPath)$($DirectorySeparatorChar)$($previous.Name)' OR it does not exist (*$($ManagementGroupId)_ResourcesAll.csv)" + $doResourceFluctuation = $false + } + + if ($doResourceFluctuation) { + #$importPrevious.Count + + #https://gist.github.com/fatherjack/4c91cc6832b8b02d1b7319716a5fba52 + function Compare-StringSet { + <# + .SYNOPSIS + Compare two sets of strings and see the matched and unmatched elements from each input + + .DESCRIPTION + Compares sets of + + .PARAMETER Ref + The reference set of values to be compared + + .PARAMETER Diff + The difference set of values to be compared + + .PARAMETER CaseSensitive + Enables a case-sensitive comparison + + .EXAMPLE + $ref, $dif = @( + , @('a', 'b', 'c') + , @('b', 'c', 'd') + ) + $Sets = Compare-StringSet $ref $dif + $Sets.RefOnly + + $Sets.DiffOnly + + $Sets.Both + + This example sets up two arrays with some similar values and then passes them both to the Compare-StringSet function. the results of this are stored in the variable $Sets. + $Sets is an object that has three properties - RefOnly, DiffOnly, and Both. These are sets of the incoming values where they intersect or not. + + .EXAMPLE + $ref, $dif = @( + , @('tree', 'house', 'football') + , @('dog', 'cat', 'tree', 'house', 'Football') + ) + $Sets = Compare-StringSet $ref $dif -CaseSensitive + $Sets.RefOnly + $Sets.DiffOnly + $Sets.Both + + This example sets up two arrays with some similar values and then passes them both to the Compare-StringSet function using the -CaseSensitive switch. The results of this are stored in the variable $Sets. + $Sets is an object that has three properties - RefOnly, DiffOnly, and Both. + + Because of the -CaseSensitive switch usage 'football' is shown as in RefOnly and 'Football' is shown as in DiffOnly. + + .NOTES + From https://gist.github.com/IISResetMe/57ce7b76e1001974a4f7170e10775875 + #> + + param( + [string[]]$Ref, + [string[]]$Diff, + + [switch]$CaseSensitive + ) + + $Comparer = if ($CaseSensitive) { + [System.StringComparer]::InvariantCulture + } + else { + [System.StringComparer]::InvariantCultureIgnoreCase + } + + $Results = [ordered]@{ + RefOnly = @() + Both = @() + DiffOnly = @() + } + + $temp = [System.Collections.Generic.HashSet[string]]::new($Ref, $Comparer) + $temp.IntersectWith($Diff) + $Results['Both'] = $temp + + #$temp = [System.Collections.Generic.HashSet[string]]::new($Ref, [System.StringComparer]::CurrentCultureIgnoreCase) + $temp = [System.Collections.Generic.HashSet[string]]::new($Ref, $Comparer) + $temp.ExceptWith($Diff) + $Results['RefOnly'] = $temp + + #$temp = [System.Collections.Generic.HashSet[string]]::new($Diff, [System.StringComparer]::CurrentCultureIgnoreCase) + $temp = [System.Collections.Generic.HashSet[string]]::new($Diff, $Comparer) + $temp.ExceptWith($Ref) + $Results['DiffOnly'] = $temp + + return [pscustomobject]$Results + } + + Write-Host " Comparing previous ($($importPrevious.Count)) with latest ($($resourcesIdsAll.Count))" + $start = Get-Date + $x = Compare-StringSet $importPrevious $resourcesIdsAll.id + Write-Host ' unique values in previous (deleted):' $x.RefOnly.Count + Write-Host " values that are contained in previous and latest: $($x.Both.Count)" + Write-Host ' unique values in latest (added):' $x.DiffOnly.Count + $end = Get-Date + Write-Host " Compare previous with latest duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) mins ($((New-TimeSpan -Start $start -End $end).TotalSeconds) sec)" + + $script:arrayResourceFluctuationFinal = [System.Collections.ArrayList]@() + + #ADDED + $arrayAdded = [System.Collections.ArrayList]@() + $arrayAddedAndRemoved = [System.Collections.ArrayList]@() + foreach ($resource in $x.DiffOnly) { + $resourceSplitted = $resource.split('/') + #$resourceSplitted + + $null = $arrayAdded.Add([PSCustomObject]@{ + subscriptionId = $resourceSplitted[2] + resourceType0 = $resourceSplitted[6] + resourceType1 = $resourceSplitted[7] + resourceType2 = $resourceSplitted[9] + resourceType3 = $resourceSplitted[11] + }) + + $subDetails = $htSubscriptionsMgPath.($resourceSplitted[2]) + $null = $arrayAddedAndRemoved.Add([pscustomobject]@{ + action = 'add' + subscriptionId = $resourceSplitted[2] + subscriptionName = $subDetails.displayName + mgPath = $subDetails.pathDelimited + resourceId = $resource + resourceType0 = $resourceSplitted[6] + resourceType1 = $resourceSplitted[7] + resourceType2 = $resourceSplitted[9] + resourceType3 = $resourceSplitted[11] + }) + + if ($resourceSplitted.Count -gt 13) { + Write-Host ' Unforeseen Resource type!' + Write-Host " Please report this Resource type at $($GithubRepository): '$resource'" + } + } + + if ($arrayAdded.Count -gt 0) { + $arrayGroupedByResourceType = $arrayAdded | Group-Object -Property resourceType0, resourceType1, resourceType2, resourceType3 + foreach ($resourceType in $arrayGroupedByResourceType) { + $arrayGroupedBySubscription = $arrayGroupedByResourceType.where({ $_.Name -eq $resourceType.Name }).Group | Group-Object -Property subscriptionId | Select-Object -ExcludeProperty Group + $null = $arrayResourceFluctuationFinal.Add([PSCustomObject]@{ + Event = 'Added' + ResourceType = ($resourceType.Name -replace ', ', '/') + 'Resource count' = $resourceType.Count + 'Subscription count' = ($arrayGroupedBySubscription | Measure-Object).Count + }) + } + } + + #REMOVED + $arrayRemoved = [System.Collections.ArrayList]@() + foreach ($resource in $x.RefOnly) { + $resourceSplitted = $resource.split('/') + #$resourceSplitted + + $null = $arrayRemoved.Add([PSCustomObject]@{ + subscriptionId = $resourceSplitted[2] + resourceType0 = $resourceSplitted[6] + resourceType1 = $resourceSplitted[7] + resourceType2 = $resourceSplitted[9] + resourceType3 = $resourceSplitted[11] + }) + + $subDetails = $htSubscriptionsMgPath.($resourceSplitted[2]) + $null = $arrayAddedAndRemoved.Add([pscustomobject]@{ + action = 'remove' + subscriptionId = $resourceSplitted[2] + subscriptionName = $subDetails.displayName + mgPath = $subDetails.pathDelimited + resourceId = $resource + resourceType0 = $resourceSplitted[6] + resourceType1 = $resourceSplitted[7] + resourceType2 = $resourceSplitted[9] + resourceType3 = $resourceSplitted[11] + }) + + if ($resourceSplitted.Count -gt 13) { + Write-Host ' Unforeseen Resource type!' + Write-Host " Please report this Resource type at $($GithubRepository): '$resource'" + } + } + + if ($arrayRemoved.Count -gt 0) { + $arrayGroupedByResourceType = $arrayRemoved | Group-Object -Property resourceType0, resourceType1, resourceType2, resourceType3 + foreach ($resourceType in $arrayGroupedByResourceType) { + $arrayGroupedBySubscription = $arrayGroupedByResourceType.where({ $_.Name -eq $resourceType.Name }).Group | Group-Object -Property subscriptionId | Select-Object -ExcludeProperty Group + $null = $arrayResourceFluctuationFinal.Add([PSCustomObject]@{ + Event = 'Removed' + ResourceType = ($resourceType.Name -replace ', ', '/') + 'Resource count' = $resourceType.Count + 'Subscription count' = ($arrayGroupedBySubscription | Measure-Object).Count + }) + } + } + } + } + else { + Write-Host " Process Resource fluctuation skipped, no previous output (*$($ManagementGroupId)_ResourcesAll.csv) found" + } + + if ($arrayResourceFluctuationFinal.Count -gt 0 -and $doResourceFluctuation) { + if (-not $NoCsvExport) { + #DataCollection Export of Resource fluctuation + Write-Host " Exporting ResourceFluctuation CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuation.csv'" + $arrayResourceFluctuationFinal | Sort-Object -Property ResourceType | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuation.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + + Write-Host " Exporting ResourceFluctuation detailed CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuationDetailed.csv'" + $arrayAddedAndRemoved | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourceFluctuationDetailed.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + Write-Host "Process Resource fluctuation duration: $((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds) seconds" + + #DataCollection Export of All Resources + if ($resourcesIdsAll.Count -gt 0) { + if (-not $NoCsvExport) { + Write-Host "Exporting ResourcesAll CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv'" + $resourcesIdsAll | Sort-Object -Property id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesAll.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + else { + Write-Host "Not Exporting ResourcesAll CSV, as there are $($resourcesIdsAll.Count) resources" + } + } + } + } + + if ($azAPICallConf['htParameters'].LargeTenant -eq $false -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -eq $false -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $false) { + if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + addRowToTable ` + -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain | Measure-Object).Count - 1) ` + -mgName $getMgParentName ` + -mgId $getMgParentId ` + -mgParentId "'upperScopes'" ` + -mgParentName 'upperScopes' + } + } + + if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -eq $true) { + if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + $currentTask = "Policy assignments ('$($ManagementGroupId)')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($ManagementGroupId)/providers/Microsoft.Authorization/policyAssignments?`$filter=atScope()&api-version=2021-06-01" + $method = 'GET' + $upperScopesPolicyAssignments = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $upperScopesPolicyAssignments = $upperScopesPolicyAssignments | Where-Object { $_.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" } + $upperScopesPolicyAssignmentsPolicyCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' })).count + $upperScopesPolicyAssignmentsPolicySetCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' })).count + $upperScopesPolicyAssignmentsPolicyAtScopeCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count + $upperScopesPolicyAssignmentsPolicySetAtScopeCount = (($upperScopesPolicyAssignments | Where-Object { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count + $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount = ($upperScopesPolicyAssignmentsPolicyAtScopeCount + $upperScopesPolicyAssignmentsPolicySetAtScopeCount) + foreach ($L0mgmtGroupPolicyAssignment in $upperScopesPolicyAssignments) { + + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -OR $L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/') { + $PolicyVariant = 'Policy' + $Id = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() + $Def = ($htCacheDefinitionsPolicy).($Id) + $PolicyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope + #$PolicyAssignmentNotScopes = $L0mgmtGroupPolicyAssignment.Properties.NotScopes -join "$CsvDelimiterOpposite " + $PolicyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() + $PolicyAssignmentName = $L0mgmtGroupPolicyAssignment.Name + $PolicyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName + if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { + $PolicyAssignmentDescription = 'no description given' + } + else { + $PolicyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description + } + + if ($L0mgmtGroupPolicyAssignment.identity) { + $PolicyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId + } + else { + $PolicyAssignmentIdentity = 'n/a' + } + + if ($Def.Type -eq 'Custom') { + $policyDefintionScope = $Def.Scope + $policyDefintionScopeMgSub = $Def.ScopeMgSub + $policyDefintionScopeId = $Def.ScopeId + } + else { + $policyDefintionScope = 'n/a' + $policyDefintionScopeMgSub = 'n/a' + $policyDefintionScopeId = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L0mgmtGroupPolicyAssignment.properties.metadata) { + if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn + } + } + + if (($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message) { + $nonComplianceMessage = ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + #mgSecureScore + $mgAscSecureScoreResult = '' + + addRowToTable ` + -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) ` + -mgName $getMgParentName ` + -mgId $getMgParentId ` + -mgParentId "'upperScopes'" ` + -mgParentName 'upperScopes' ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Policy $Def.DisplayName ` + -PolicyDescription $Def.Description ` + -PolicyVariant $PolicyVariant ` + -PolicyType $Def.Type ` + -PolicyCategory $Def.Category ` + -PolicyDefinitionIdGuid (($Def.Id) -replace '.*/') ` + -PolicyDefinitionId $Def.PolicyDefinitionId ` + -PolicyDefintionScope $policyDefintionScope ` + -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` + -PolicyDefintionScopeId $policyDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` + -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` + -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` + -PolicyDefinitionEffectDefault ($htCacheDefinitionsPolicy).(($Def.PolicyDefinitionId)).effectDefaultValue ` + -PolicyDefinitionEffectFixed ($htCacheDefinitionsPolicy).(($Def.PolicyDefinitionId)).effectFixedValue ` + -PolicyAssignmentScope $PolicyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg 'Mg' ` + -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $PolicyAssignmentId ` + -PolicyAssignmentName $PolicyAssignmentName ` + -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` + -PolicyAssignmentDescription $PolicyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages ` + -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` + -PolicyAssignmentCount $upperScopesPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` + -PolicySetAssignmentCount $upperScopesPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + $PolicyVariant = 'PolicySet' + $Id = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() + $Def = ($htCacheDefinitionsPolicySet).($Id) + $PolicyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope + #$PolicyAssignmentNotScopes = $L0mgmtGroupPolicyAssignment.Properties.NotScopes -join "$CsvDelimiterOpposite " + $PolicyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() + $PolicyAssignmentName = $L0mgmtGroupPolicyAssignment.Name + $PolicyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName + if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { + $PolicyAssignmentDescription = 'no description given' + } + else { + $PolicyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description + } + + if ($L0mgmtGroupPolicyAssignment.identity) { + $PolicyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId + } + else { + $PolicyAssignmentIdentity = 'n/a' + } + + if ($Def.Type -eq 'Custom') { + $policyDefintionScope = $Def.Scope + $policyDefintionScopeMgSub = $Def.ScopeMgSub + $policyDefintionScopeId = $Def.ScopeId + } + else { + $policyDefintionScope = 'n/a' + $policyDefintionScopeMgSub = 'n/a' + $policyDefintionScopeId = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L0mgmtGroupPolicyAssignment.properties.metadata) { + if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn + } + } + + if (($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message) { + $nonComplianceMessage = ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + addRowToTable ` + -level (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) ` + -mgName $getMgParentName ` + -mgId $getMgParentId ` + -mgParentId "'upperScopes'" ` + -mgParentName 'upperScopes' ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Policy $Def.DisplayName ` + -PolicyDescription $Def.Description ` + -PolicyVariant $PolicyVariant ` + -PolicyType $Def.Type ` + -PolicyCategory $Def.Category ` + -PolicyDefinitionIdGuid (($Def.Id) -replace '.*/') ` + -PolicyDefinitionId $Def.PolicyDefinitionId ` + -PolicyDefintionScope $policyDefintionScope ` + -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` + -PolicyDefintionScopeId $policyDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` + -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` + -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` + -PolicyAssignmentScope $PolicyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg 'Mg' ` + -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $PolicyAssignmentId ` + -PolicyAssignmentName $PolicyAssignmentName ` + -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` + -PolicyAssignmentDescription $PolicyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages ` + -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` + -PolicyAssignmentCount $upperScopesPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` + -PolicySetAssignmentCount $upperScopesPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $upperScopesPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + } + } + } + } + + if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $true) { + + #RoleAssignment API (system metadata e.g. createdOn) + $currentTask = "Role assignments API '$($ManagementGroupId)'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" + $method = 'GET' + $roleAssignmentsFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($roleAssignmentsFromAPI.Count -gt 0) { + $principalsToResolve = @() + $principalsToResolve = foreach ($ra in $roleAssignmentsFromAPI.properties | Sort-Object -Property principalId -Unique) { + if (-not $htPrincipals.($ra.principalId)) { + $ra.principalId + } + } + + if ($principalsToResolve.Count -gt 0) { + ResolveObjectIds -objectIds $principalsToResolve + } + } + + #$upperScopesRoleAssignments = GetRoleAssignments -Scope "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" -scopeDetails "getRoleAssignments upperScopes (Mg)" + $upperScopesRoleAssignments = $roleAssignmentsFromAPI + + $upperScopesRoleAssignmentsLimitUtilization = (($upperScopesRoleAssignments | Where-Object { $_.properties.scope -eq "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)" })).count + #tenantLevelRoleAssignments + if (-not $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments') { + $tenantLevelRoleAssignmentsCount = (($upperScopesRoleAssignments | Where-Object { $_.id -like '/providers/Microsoft.Authorization/roleAssignments/*' })).count + $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments' = @{} + $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments'.AssignmentsCount = $tenantLevelRoleAssignmentsCount + } + + foreach ($upperScopesRoleAssignment in $upperScopesRoleAssignments) { + + $roleAssignmentId = ($upperScopesRoleAssignment.id).ToLower() + + if ($upperScopesRoleAssignment.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$ManagementGroupId") { + $roleDefinitionId = $upperScopesRoleAssignment.properties.roleDefinitionId + $roleDefinitionIdGuid = $roleDefinitionId -replace '.*/' + + if (-not ($htCacheDefinitionsRole).($roleDefinitionIdGuid)) { + $roleDefinitionName = "'This roleDefinition likely was deleted although a roleAssignment existed'" + } + else { + $roleDefinitionName = ($htCacheDefinitionsRole).($roleDefinitionIdGuid).Name + } + + if (($htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName).length -eq 0) { + $roleAssignmentIdentityDisplayname = 'n/a' + } + else { + if ($htPrincipals.($upperScopesRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentityDisplayname = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName + } + else { + $roleAssignmentIdentityDisplayname = 'scrubbed' + } + } + else { + $roleAssignmentIdentityDisplayname = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).displayName + } + } + if (-not $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName) { + $roleAssignmentIdentitySignInName = 'n/a' + } + else { + if ($htPrincipals.($upperScopesRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentitySignInName = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName + } + else { + $roleAssignmentIdentitySignInName = 'scrubbed' + } + } + else { + $roleAssignmentIdentitySignInName = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).signInName + } + } + $roleAssignmentIdentityObjectId = $upperScopesRoleAssignment.properties.principalId + $roleAssignmentIdentityObjectType = $htPrincipals.($upperScopesRoleAssignment.properties.principalId).type + + $roleAssignmentScope = $upperScopesRoleAssignment.properties.scope + $roleAssignmentScopeName = $roleAssignmentScope -replace '.*/' + $roleAssignmentScopeType = 'MG' + + $roleSecurityCustomRoleOwner = 0 + if (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True) { + $roleSecurityCustomRoleOwner = 1 + } + $roleSecurityOwnerAssignmentSP = 0 + if ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Id -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal') -or (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal')) { + $roleSecurityOwnerAssignmentSP = 1 + } + + $createdBy = '' + $createdOn = '' + $createdOnUnformatted = $null + $updatedBy = '' + $updatedOn = '' + + if ($upperScopesRoleAssignment.properties.createdBy) { + $createdBy = $upperScopesRoleAssignment.properties.createdBy + } + if ($upperScopesRoleAssignment.properties.createdOn) { + $createdOn = $upperScopesRoleAssignment.properties.createdOn + } + if ($upperScopesRoleAssignment.properties.updatedBy) { + $updatedBy = $upperScopesRoleAssignment.properties.updatedBy + } + if ($upperScopesRoleAssignment.properties.updatedOn) { + $updatedOn = $upperScopesRoleAssignment.properties.updatedOn + } + $createdOnUnformatted = $upperScopesRoleAssignment.properties.createdOn + + if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + $levelToUse = (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count - 1) + $toUseAsmgName = $getMgParentName + $toUseAsmgId = $getMgParentId + $toUseAsmgParentId = "'upperScopes'" + $toUseAsmgParentName = 'upperScopes' + } + else { + $levelToUse = (($htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain).Count) + $toUseAsmgName = $selectedManagementGroupId.DisplayName + $toUseAsmgId = $selectedManagementGroupId.Name + $toUseAsmgParentId = 'Tenant' + $toUseAsmgParentName = 'Tenant' + } + + #mgSecureScore + $mgAscSecureScoreResult = '' + + addRowToTable ` + -level $levelToUse ` + -mgName $toUseAsmgName ` + -mgId $toUseAsmgId ` + -mgParentId $toUseAsmgParentId ` + -mgParentName $toUseAsmgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -RoleDefinitionId $roleDefinitionIdGuid ` + -RoleDefinitionName $roleDefinitionName ` + -RoleIsCustom ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom ` + -RoleAssignableScopes (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).AssignableScopes -join "$CsvDelimiterOpposite ") ` + -RoleActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -join "$CsvDelimiterOpposite ") ` + -RoleNotActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions -join "$CsvDelimiterOpposite ") ` + -RoleDataActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).DataActions -join "$CsvDelimiterOpposite ") ` + -RoleNotDataActions (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotDataActions -join "$CsvDelimiterOpposite ") ` + -RoleAssignmentIdentityDisplayname $roleAssignmentIdentityDisplayname ` + -RoleAssignmentIdentitySignInName $roleAssignmentIdentitySignInName ` + -RoleAssignmentIdentityObjectId $roleAssignmentIdentityObjectId ` + -RoleAssignmentIdentityObjectType $roleAssignmentIdentityObjectType ` + -RoleAssignmentId $roleAssignmentId ` + -RoleAssignmentScope $roleAssignmentScope ` + -RoleAssignmentScopeName $roleAssignmentScopeName ` + -RoleAssignmentScopeType $roleAssignmentScopeType ` + -RoleAssignmentCreatedBy $createdBy ` + -RoleAssignmentCreatedOn $createdOn ` + -RoleAssignmentCreatedOnUnformatted $createdOnUnformatted ` + -RoleAssignmentUpdatedBy $updatedBy ` + -RoleAssignmentUpdatedOn $updatedOn ` + -RoleAssignmentsLimit $LimitRBACRoleAssignmentsManagementGroup ` + -RoleAssignmentsCount $upperScopesRoleAssignmentsLimitUtilization ` + -RoleSecurityCustomRoleOwner $roleSecurityCustomRoleOwner ` + -RoleSecurityOwnerAssignmentSP $roleSecurityOwnerAssignmentSP ` + -RoleAssignmentPIM 'unknown' + } + } + } +} +function processDefinitionInsights() { + $startDefinitionInsights = Get-Date + Write-Host ' Building DefinitionInsights' + + $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + $utf8 = New-Object -TypeName System.Text.UTF8Encoding + + #region definitionInsightsAzurePolicy + $htmlDefinitionInsights = [System.Text.StringBuilder]::new() + [void]$htmlDefinitionInsights.AppendLine( @' + +
+'@) + + #policy/policySet preQuery + #region preQuery + $htPolicyWithAssignments = @{} + $htPolicyWithAssignments.policy = @{} + $htPolicyWithAssignments.policySet = @{} + + foreach ($policyOrPolicySet in $arrayPolicyAssignmentsEnriched | Sort-Object -Property PolicyAssignmentId -Unique | Group-Object -Property PolicyId, PolicyVariant) { + $policyOrPolicySetNameSplit = $policyOrPolicySet.name.split(', ') + if ($policyOrPolicySetNameSplit[1] -eq 'Policy') { + #policy + if (-not ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0])) { + $pscustomObj = [System.Collections.ArrayList]@() + foreach ($entry in $policyOrPolicySet.group) { + $null = $pscustomObj.Add([PSCustomObject]@{ + PolicyAssignmentId = $entry.PolicyAssignmentId + PolicyAssignmentDisplayName = $entry.PolicyAssignmentDisplayName + }) + } + ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0]) = @{} + ($htPolicyWithAssignments).policy.($policyOrPolicySetNameSplit[0]).Assignments = [array]($pscustomObj) + } + } + else { + #policySet + if (-not ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0])) { + $pscustomObj = [System.Collections.ArrayList]@() + foreach ($entry in $policyOrPolicySet.group) { + $null = $pscustomObj.Add([PSCustomObject]@{ + PolicyAssignmentId = $entry.PolicyAssignmentId + PolicyAssignmentDisplayName = $entry.PolicyAssignmentDisplayName + }) + } + ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0]) = @{} + ($htPolicyWithAssignments).policySet.($policyOrPolicySetNameSplit[0]).Assignments = [array]($pscustomObj) + } + } + } + + foreach ($customPolicy in $tenantCustomPolicies) { + if ($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId)) { + if (-not ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId)) { + ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId) = @{} + ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments) + } + else { + $array = @() + $array += ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments + $array += $htPoliciesWithAssignmentOnRgRes.($customPolicy.PolicyDefinitionId).Assignments + ($htPolicyWithAssignments).policy.($customPolicy.PolicyDefinitionId).Assignments = $array + } + } + } + + foreach ($customPolicySet in $tenantCustomPolicySets) { + if ($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId)) { + if (-not ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId)) { + ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId) = @{} + ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = [array]($htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments) + } + else { + $array = @() + $array += ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments + $array += $htPoliciesWithAssignmentOnRgRes.($customPolicySet.PolicyDefinitionId).Assignments + ($htPolicyWithAssignments).policySet.($customPolicySet.PolicyDefinitionId).Assignments = $array + } + } + } + #endregion preQuery + + #region definitionInsightsPolicyDefinitions + $startDefinitionInsightsPolicyDefinitions = Get-Date + Write-Host ' processing DefinitionInsights Policy definitions' + ShowMemoryUsage + $tfCount = $tenantAllPoliciesCount + $htmlTableId = 'definitionInsights_Policy' + [void]$htmlDefinitionInsights.AppendLine( @" + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+ + +
+ + + + + +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +"@) + + $cnter = 0 + $htmlDefinitionInsightshlp = $null + $htmlDefinitionInsightshlp = foreach ($policy in (($htCacheDefinitionsPolicy).Values | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + + $cnter++ + if ($cnter % 1000 -eq 0) { + Write-Host " $cnter Policy definitions processed" + ShowMemoryUsage + } + + $hasAssignments = 'false' + $assignmentsCount = 0 + $assignmentsDetailed = 'n/a' + + if (($htPolicyWithAssignments).policy.($policy.PolicyDefinitionId)) { + $hasAssignments = 'true' + $assignments = ($htPolicyWithAssignments).policy.($policy.PolicyDefinitionId).Assignments + $assignmentsCount = $assignments.Count + + if ($assignmentsCount -gt 0) { + $arrayAssignmentDetails = @() + $arrayAssignmentDetails = foreach ($assignment in $assignments) { + if ($assignment.PolicyAssignmentDisplayName -eq '') { + $polAssDisplayName = '#no AssignmentName given' + } + else { + $polAssDisplayName = $assignment.PolicyAssignmentDisplayName + } + "$($assignment.PolicyAssignmentId) ($($polAssDisplayName))" + } + $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " + } + + } + + $roleDefinitionIds = 'n/a' + if ($policy.RoleDefinitionIds -ne 'n/a') { + $arrayRoleDefDetails = @() + $arrayRoleDefDetails = foreach ($roleDef in $policy.RoleDefinitionIds) { + $roleDefIdOnly = $roleDef -replace '.*/' + if (($roleDefIdOnly).Length -ne 36) { + "'INVALID RoleDefId!' ($($roleDefIdOnly))" + } + else { + $roleDefHlp = ($htCacheDefinitionsRole).($roleDefIdOnly) + "'$($roleDefHlp.Name)' ($($roleDefHlp.Id))" + } + } + $roleDefinitionIds = $arrayRoleDefDetails -join "$CsvDelimiterOpposite " + } + + $scopeDetails = 'n/a' + if ($policy.ScopeId -ne 'n/a') { + if ([string]::IsNullOrEmpty($policy.ScopeId)) { + Write-Host "unexpected IsNullOrEmpty - processing: $($policy | ConvertTo-Json -Depth 99)" + } + $scopeDetails = "$($policy.ScopeId) ($($htEntities.($policy.ScopeId).DisplayName))" + } + + $usedInPolicySet = 'false' + $usedInPolicySetCount = 0 + $usedInPolicySets = 'n/a' + + if ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId)) { + $usedInPolicySet = 'true' + $usedInPolicySetCount = ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId).policySet).Count + $usedInPolicySets = ($htPoliciesUsedInPolicySets.($policy.PolicyDefinitionId).policySet | Sort-Object) -join "$CsvDelimiterOpposite " + } + + $json = $($policy.Json | ConvertTo-Json -Depth 99) + $guid = ([System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($policy.PolicyDefinitionId)))) -replace '-' + @" + + + + + + + + + + + + + + + + + + +"@ + } + [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) + if ($NoDefinitionInsightsDedicatedHTML) { + $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlDefinitionInsights = [System.Text.StringBuilder]::new() + } + [void]$htmlDefinitionInsights.AppendLine( @" + +
JSONPolicyTypeALZCategoryDeprecatedPreviewScope Mg/SubScope Name/IdeffectDefaultValuehasAssignmentsAssignments CountAssignmentsUsedInPolicySetPolicySetsCountPolicySetsRoles
+ +
+ + +
+ +
+ + + +
$($policy.Type)$($policy.ALZ)$($policy.Category -replace '<', '<' -replace '>', '>')$($policy.Deprecated)$($policy.Preview)$($policy.ScopeMgSub)$($scopeDetails -replace '<', '<' -replace '>', '>')$($policy.effectDefaultValue)$hasAssignments$assignmentsCount$assignmentsDetailed$usedInPolicySet$usedInPolicySetCount$usedInPolicySets$($roleDefinitionIds -replace '<', '<' -replace '>', '>')
+
+ +
+"@) + $endDefinitionInsightsPolicyDefinitions = Get-Date + Write-Host " DefinitionInsights Policy definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsPolicyDefinitions -End $endDefinitionInsightsPolicyDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsPolicyDefinitions -End $endDefinitionInsightsPolicyDefinitions).TotalSeconds) seconds)" + showMemoryUsage + #endregion definitionInsightsPolicyDefinitions + + #region definitionInsightsPolicySetDefinitions + $startDefinitionInsightsPolicySetDefinitions = Get-Date + Write-Host ' processing DefinitionInsights PolicySet definitions' + ShowMemoryUsage + $tfCount = $tenantAllPolicySetsCount + $htmlTableId = 'definitionInsights_PolicySet' + [void]$htmlDefinitionInsights.AppendLine( @" + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + +"@) + $htmlDefinitionInsightshlp = $null + $htmlDefinitionInsightshlp = foreach ($policySet in ($tenantAllPolicySets | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + $hasAssignments = 'false' + $assignmentsCount = 0 + $assignmentsDetailed = 'n/a' + + if (($htPolicyWithAssignments).policySet.($policySet.PolicyDefinitionId)) { + $hasAssignments = 'true' + $assignments = ($htPolicyWithAssignments).policySet.($policySet.PolicyDefinitionId).Assignments + $assignmentsCount = ($assignments | Measure-Object).Count + + if ($assignmentsCount -gt 0) { + $arrayAssignmentDetails = @() + $arrayAssignmentDetails = foreach ($assignment in $assignments) { + if ($assignment.PolicyAssignmentDisplayName -eq '') { + $polAssDisplayName = '#no AssignmentName given' + } + else { + $polAssDisplayName = $assignment.PolicyAssignmentDisplayName + } + "$($assignment.PolicyAssignmentId) ($($polAssDisplayName))" + } + $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " + } + } + + $scopeDetails = 'n/a' + if ($policySet.ScopeId -ne 'n/a') { + $scopeDetails = "$($policySet.ScopeId) ($($htEntities.($policySet.ScopeId).DisplayName))" + } + $json = $($policySet.Json | ConvertTo-Json -Depth 99) + $guid = ([System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($policySet.PolicyDefinitionId)))) -replace '-' + @" + + + + + + + + + + + + + +"@ + } + [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) + if ($NoDefinitionInsightsDedicatedHTML) { + $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlDefinitionInsights = [System.Text.StringBuilder]::new() + } + [void]$htmlDefinitionInsights.AppendLine( @" + +
JSONPolicySet TypeALZCategoryDeprecatedPreviewScope Mg/SubScope Name/IdhasAssignmentsAssignments CountAssignments
+ +
+ + +
+ +
+ + + +
$($policySet.Type)$($policySet.ALZ)$($policySet.Category -replace '<', '<' -replace '>', '>')$($policySet.Deprecated)$($policySet.Preview)$($policySet.ScopeMgSub)$($scopeDetails -replace '<', '<' -replace '>', '>')$hasAssignments$assignmentsCount$assignmentsDetailed
+
+ +
+"@) + $endDefinitionInsightsPolicySetDefinitions = Get-Date + Write-Host " DefinitionInsights PolicySet definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsPolicySetDefinitions -End $endDefinitionInsightsPolicySetDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsPolicySetDefinitions -End $endDefinitionInsightsPolicySetDefinitions).TotalSeconds) seconds)" + showMemoryUsage + #endregion definitionInsightsPolicySetDefinitions + + [void]$htmlDefinitionInsights.AppendLine( @' +
+'@) + #endregion definitionInsightsAzurePolicy + + #region definitionInsightsAzureRBAC + [void]$htmlDefinitionInsights.AppendLine( @' + +
+'@) + + #RBAC preQuery + $htRoleWithAssignments = @{} + foreach ($roleDef in $rbacAll | Sort-Object -Property RoleAssignmentId -Unique | Group-Object -Property RoleId) { + if (-not ($htRoleWithAssignments).($roleDef.Name)) { + ($htRoleWithAssignments).($roleDef.Name) = @{} + ($htRoleWithAssignments).($roleDef.Name).Assignments = $roleDef.group + } + } + + #region definitionInsightsRoleDefinitions + $startDefinitionInsightsRoleDefinitions = Get-Date + Write-Host ' processing DefinitionInsights Role definitions' + ShowMemoryUsage + $tfCount = $tenantAllRolesCount + $htmlTableId = 'definitionInsights_Roles' + [void]$htmlDefinitionInsights.AppendLine( @" + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ + + + + + + + + + + + + + +"@) + $arrayRoleDefinitionsForCSVExport = [System.Collections.ArrayList]@() + $htmlDefinitionInsightshlp = $null + $htmlDefinitionInsightshlp = foreach ($role in ($tenantAllRoles | Sort-Object @{Expression = { $_.Name } })) { + if ($role.IsCustom -eq $true) { + $roleType = 'Custom' + $AssignableScopesCount = $role.AssignableScopes.Count + if ($role.AssignableScopes -like '*/providers/microsoft.management/managementgroups/*') { + $AssignableScopesMG = $true + } + else { + $AssignableScopesMG = $false + } + + } + else { + $roleType = 'Builtin' + $AssignableScopesCount = '' + $AssignableScopesMG = '' + } + if (-not [string]::IsNullOrEmpty($role.DataActions) -or -not [string]::IsNullOrEmpty($role.NotDataActions)) { + $roleManageData = 'true' + } + else { + $roleManageData = 'false' + } + + $hasAssignments = 'false' + $assignmentsCount = 0 + $assignmentsDetailed = 'n/a' + if (($htRoleWithAssignments).($role.Id)) { + $hasAssignments = 'true' + $assignments = ($htRoleWithAssignments).($role.Id).Assignments + $assignmentsCount = ($assignments).Count + if ($assignmentsCount -gt 0) { + $arrayAssignmentDetails = @() + $arrayAssignmentDetails = foreach ($assignment in $assignments) { + "$($assignment.RoleAssignmentId)" + } + $assignmentsDetailed = $arrayAssignmentDetails -join "$CsvDelimiterOpposite " + } + } + + #array for exportCSV + if (-not $NoCsvExport) { + $null = $arrayRoleDefinitionsForCSVExport.Add([PSCustomObject]@{ + Name = $role.Name + Id = $role.Id + Description = $role.Json.description + Type = $roleType + AssignmentsCount = $assignmentsCount + AssignableScopesCount = $AssignableScopesCount + AssignableScopesMG = $AssignableScopesMG + AssignableScopes = ($role.AssignableScopes | Sort-Object) -join "$CsvDelimiterOpposite " + DataRelated = $roleManageData + RoleAssWriteCapable = $role.RoleCanDoRoleAssignments + Actions = $role.Actions -join "$CsvDelimiterOpposite " + NotActions = $role.NotActions -join "$CsvDelimiterOpposite " + DataActions = $role.DataActions -join "$CsvDelimiterOpposite " + NotDataActions = $role.NotDataActions -join "$CsvDelimiterOpposite " + }) + } + + $json = $role.Json | ConvertTo-Json -Depth 99 + $guid = $role.Id -replace '-' + @" + + + + + + + + + +"@ + } + + #region exportCSV + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_RoleDefinitions" + Write-Host " Exporting RoleDefinitions CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $arrayRoleDefinitionsForCSVExport | Sort-Object -Property Type, Name, Id | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + $arrayRoleDefinitionsForCSVExport = $null + } + #endregion exportCSV + + [void]$htmlDefinitionInsights.AppendLine($htmlDefinitionInsightshlp) + if ($NoDefinitionInsightsDedicatedHTML) { + $htmlDefinitionInsights | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlDefinitionInsights = [System.Text.StringBuilder]::new() + } + [void]$htmlDefinitionInsights.AppendLine( @" + +
JSONRole TypeDatacanDoRoleAssignmentshasAssignmentsAssignments CountAssignments
+ +
+ + +
+ +
+ + + +
$($roleType)$($roleManageData)$($role.RoleCanDoRoleAssignments)$hasAssignments$assignmentsCount$assignmentsDetailed
+
+ +
+"@) + $endDefinitionInsightsRoleDefinitions = Get-Date + Write-Host " DefinitionInsights Role definitions duration: $((New-TimeSpan -Start $startDefinitionInsightsRoleDefinitions -End $endDefinitionInsightsRoleDefinitions).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsightsRoleDefinitions -End $endDefinitionInsightsRoleDefinitions).TotalSeconds) seconds)" + showMemoryUsage + #endregion definitionInsightsRoleDefinitions + + [void]$htmlDefinitionInsights.AppendLine( @' +
+'@) + #endregion definitionInsightsAzureRBAC + + Write-Host " NoDefinitionInsightsDedicatedHTML: $NoDefinitionInsightsDedicatedHTML" + if ($NoDefinitionInsightsDedicatedHTML) { + Write-Host ' Appending DefinitionInsights to HTML' + $script:html += $htmlDefinitionInsights + $htmlDefinitionInsights = $null + $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $script:html = $null + } + else { + Write-Host " Creating dedicated DefinitionInsights HTML ($($outputPath)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html)" + $htmlDefinitionInsightsDedicated = $null + $htmlDefinitionInsightsDedicated += $htmlDefinitionInsightsDedicatedStart + $htmlDefinitionInsightsDedicated += $htmlDefinitionInsights + $htmlDefinitionInsightsDedicated += $htmlDefinitionInsightsDedicatedEnd + #$htmlDefinitionInsights = $null + $htmlDefinitionInsightsDedicated | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html" -Encoding utf8 -Force + #$script:htmlDefinitionInsightsDedicated = $null + + $htmlDefinitionInsightsNo = @" + DefinitionInsights has been saved to dedicated HTML file '$($outputPathGiven)$($DirectorySeparatorChar)$($fileName)_DefinitionInsights.html' (parameter -NoDefinitionInsightsDedicatedHTML = $($NoDefinitionInsightsDedicatedHTML))
+ Open DefinitionInsights +"@ + $script:html += $htmlDefinitionInsightsNo + #$htmlDefinitionInsightsNo = $null + $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $script:html = $null + } + + + $endDefinitionInsights = Get-Date + Write-Host " DefinitionInsights processing duration: $((New-TimeSpan -Start $startDefinitionInsights -End $endDefinitionInsights).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefinitionInsights -End $endDefinitionInsights).TotalSeconds) seconds)" + ShowMemoryUsage +} +function processDiagramMermaid() { + if ($ManagementGroupId -ne $azAPICallConf['checkContext'].Tenant.Id) { + $optimizedTableForPathQueryMg = $optimizedTableForPathQueryMg.where({ $_.mgParentId -ne "'upperScopes'" }) + } + $mgLevels = ($optimizedTableForPathQueryMg | Sort-Object -Property Level -Unique).Level + + foreach ($mgLevel in $mgLevels) { + $mgsInLevel = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel } )).MgId | Get-Unique + foreach ($mgInLevel in $mgsInLevel) { + $mgDetails = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) + $mgName = $mgDetails.MgName | Get-Unique + $mgParentId = $mgDetails.mgParentId | Get-Unique + $mgParentName = $mgDetails.mgParentName | Get-Unique + if ($mgInLevel -ne $getMgParentId) { + $null = $script:arrayMgs.Add($mgInLevel) + } + + if ($mgParentName -eq $mgParentId) { + $mgParentNameId = $mgParentName + } + else { + $mgParentNameId = "$mgParentName
$mgParentId" + } + + if ($mgName -eq $mgInLevel) { + $mgNameId = $mgName + } + else { + $mgNameId = "$mgName
$mgInLevel" + } + $script:markdownhierarchyMgs += @" +$mgParentId(`"$mgParentNameId`") --> $mgInLevel(`"$mgNameId`")`n +"@ + $subsUnderMg = ($optimizedTableForPathQueryMgAndSub.where( { -not [string]::IsNullOrEmpty($_.SubscriptionId) -and $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).SubscriptionId + if (($subsUnderMg | Measure-Object).count -gt 0) { + foreach ($subUnderMg in $subsUnderMg) { + $null = $script:arraySubs.Add("SubsOf$mgInLevel") + $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) + $mgName = $mgDetalsN.MgName | Get-Unique + $mgParentId = $mgDetalsN.MgParentId | Get-Unique + $mgParentName = $mgDetalsN.MgParentName | Get-Unique + $subName = ($optimizedTableForPathQuery.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel -and $_.SubscriptionId -eq $subUnderMg } )).Subscription | Get-Unique + $script:markdownTable += @" +| $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | $subName | $($subUnderMg -replace '.*/') |`n +"@ + } + $mgName = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )).MgName | Get-Unique + if ($mgName -eq $mgInLevel) { + $mgNameId = $mgName + } + else { + $mgNameId = "$mgName
$mgInLevel" + } + $script:markdownhierarchySubs += @" +$mgInLevel(`"$mgNameId`") --> SubsOf$mgInLevel(`"$(($subsUnderMg | Measure-Object).count)`")`n +"@ + } + else { + $mgDetailsM = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.MgId -eq $mgInLevel } )) + $mgName = $mgDetailsM.MgName | Get-Unique + $mgParentId = $mgDetailsM.MgParentId | Get-Unique + $mgParentName = $mgDetailsM.MgParentName | Get-Unique + $script:markdownTable += @" +| $mgLevel | $mgName | $mgInLevel | $mgParentName | $mgParentId | none | none |`n +"@ + } + + if (($script:outOfScopeSubscriptions | Measure-Object).count -gt 0) { + $subsoosUnderMg = ($outOfScopeSubscriptions | Where-Object { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel }).SubscriptionId | Get-Unique + if (($subsoosUnderMg | Measure-Object).count -gt 0) { + foreach ($subUnderMg in $subsoosUnderMg) { + $null = $script:arraySubsOos.Add("SubsoosOf$mgInLevel") + $mgDetalsN = ($optimizedTableForPathQueryMg.where( { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel } )) + $mgName = $mgDetalsN.MgName | Get-Unique + } + $mgName = ($outOfScopeSubscriptions | Where-Object { $_.Level -eq $mgLevel -and $_.ManagementGroupId -eq $mgInLevel }).ManagementGroupName | Get-Unique + if ($mgName -eq $mgInLevel) { + $mgNameId = $mgName + } + else { + $mgNameId = "$mgName
$mgInLevel" + } + $script:markdownhierarchySubs += @" +$mgInLevel(`"$mgNameId`") --> SubsoosOf$mgInLevel(`"$(($subsoosUnderMg | Measure-Object).count)`")`n +"@ + } + } + } + } +} +function processHierarchyMapOnly { + foreach ($entity in $htEntities.values) { + if ($entity.parentNameChain -contains $ManagementGroupID -or $entity.Id -eq $ManagementGroupId) { + + if ($entity.type -eq '/subscriptions') { + $hlpEntityParent = $htEntities.(($entity.parent)) + addRowToTable ` + -level (($entity.ParentNameChain).Count - 1) ` + -mgName $hlpEntityParent.displayName ` + -mgId ($entity.parent) ` + -mgParentId $hlpEntityParent.Parent ` + -mgParentName $hlpEntityParent.ParentDisplayName ` + -Subscription $entity.DisplayName ` + -SubscriptionId $entity.Id + } + if ($entity.type -eq 'Microsoft.Management/managementGroups') { + addRowToTable ` + -level ($entity.ParentNameChain).Count ` + -mgName $entity.displayname ` + -mgId $entity.id ` + -mgParentId $entity.Parent ` + -mgParentName $entity.ParentDisplayName + } + } + } +} +function processHierarchyMapOnlyCustomData { + Write-Host 'HierarchyMapOnly with custom data' -ForegroundColor Yellow + Write-Host ' Parameter HierarchyMapOnly:' $HierarchyMapOnly + Write-Host ' Check if HierarchyMapOnlyCustomDataJSON is valid JSON' + try { + $HierarchyMapOnlyCustomDataConvertedAsHashTable = $HierarchyMapOnlyCustomDataJSON | ConvertFrom-Json -AsHashtable + $hierarchyMapOnlyCustomData = @{} + foreach ($key in $HierarchyMapOnlyCustomDataConvertedAsHashTable.Keys) { + $hierarchyMapOnlyCustomData.$key = $HierarchyMapOnlyCustomDataConvertedAsHashTable.$key | ConvertTo-Json | ConvertFrom-Json + } + Write-Host ' HierarchyMapOnlyCustomDataJSON is valid JSON' -ForegroundColor Green + } + catch { + throw 'HierarchyMapOnlyCustomDataJSON is not valid JSON' + } + + Write-Host ' Parameter hierarchyMapOnlyCustomData count:' $hierarchyMapOnlyCustomData.Keys.Count + + #validate + Write-Host ' ManagementGroupId validation' + if (-not $ManagementGroupId) { + throw 'ManagementGroupId validation failed - please provide ManagementGroupId (parameter -ManagementGroupId)' + } + else { + if ($hierarchyMapOnlyCustomData.$ManagementGroupId) { + Write-Host " ManagementGroupId '$ManagementGroupId' is available in 'hierarchyMapOnlyCustomData'" + } + else { + throw "ManagementGroupId validation failed - Given ManagementGroupId '$ManagementGroupId' is NOT available in 'hierarchyMapOnlyCustomData'" + } + Write-Host " ManagementGroupId validation passed '$ManagementGroupId'" -ForegroundColor Green + } + + Write-Host ' CustomData validation' + if ($hierarchyMapOnlyCustomData.Keys.Count -gt 0) { + Write-Host ' Checking Keys (sanity check on first item)' + $requiredKeys = @('Id', 'ParentId', 'ParentNameChain', 'ParentDisplayName', 'DisplayName', 'type') + $firstItem = $hierarchyMapOnlyCustomData.($($hierarchyMapOnlyCustomData.Keys)[0]) + foreach ($requiredKey in $requiredKeys) { + if (($firstitem | Get-Member -Name $requiredKey)) { + Write-Host " Key:$($requiredKey) exists" -ForegroundColor Green + } + else { + Write-Host " CustomData validation failed - required key:$($requiredKey) missing" -ForegroundColor DarkRed + Write-Host " The following keys are expected: $($requiredKeys -join ', ')" + throw "CustomData validation failed - required key:$($requiredKey) missing" + } + } + + Write-Host ' Checking for existence of Management Groups' + $HierarchyMapOnlyCustomDataHroupedByType = $hierarchyMapOnlyCustomData.values | Group-Object -Property type + if ($HierarchyMapOnlyCustomDataHroupedByType.Name -notcontains 'Microsoft.Management/managementGroups') { + Write-Host ' CustomData validation failed - Custom data does not contain Manangement Groups' + throw 'CustomData validation failed - Custom data does not contain Manangement Groups' + } + else { + Write-Host ' Checking for existence of Management Groups passed' -ForegroundColor Green + } + foreach ($type in $HierarchyMapOnlyCustomDataHroupedByType) { + Write-Host " Custom Data contains $($type.Count) x type: '$($type.name)'" + } + + Write-Host ' CustomData validation passed' -ForegroundColor Green + } + else { + Write-Host " CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" + throw "CustomData validation failed - no data (`$hierarchyMapOnlyCustomData.Keys.Count: $($hierarchyMapOnlyCustomData.Keys.Count))" + } + $script:htEntities = $hierarchyMapOnlyCustomData +} +function processManagedIdentities { + Write-Host 'Processing Service Principals - Managed Identities' + $startSPMI = Get-Date + $script:servicePrincipalsOfTypeManagedIdentity = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'ManagedIdentity' } ) + $script:servicePrincipalsOfTypeManagedIdentityCount = $servicePrincipalsOfTypeManagedIdentity.Count + if ($servicePrincipalsOfTypeManagedIdentityCount -gt 0) { + foreach ($sp in $servicePrincipalsOfTypeManagedIdentity) { + $hlpSp = $htServicePrincipals.($sp) + if ($hlpSp.alternativeNames -gt 0) { + foreach ($usageentry in $hlpSp.alternativeNames) { + if ($usageentry -like '*/providers/Microsoft.Authorization/policyAssignments/*') { + $script:htManagedIdentityForPolicyAssignment.($hlpSp.Id) = @{} + $script:htManagedIdentityForPolicyAssignment.($hlpSp.Id).policyAssignmentId = $usageentry.ToLower() + $script:htPolicyAssignmentManagedIdentity.($usageentry.ToLower()) = @{} + $script:htPolicyAssignmentManagedIdentity.($usageentry.ToLower()).miObjectId = $hlpSp.id + if (-not $htManagedIdentityDisplayName.($hlpSp.displayName)) { + $script:htManagedIdentityDisplayName.("$($hlpSp.displayName)_$($usageentry.ToLower())") = $hlpSp + } + } + } + } + } + } + $endSPMI = Get-Date + Write-Host "Processing Service Principals - Managed Identities duration: $((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalMinutes) minutes ($((New-TimeSpan -Start $startSPMI -End $endSPMI).TotalSeconds) seconds)" +} +function processNetwork { + $start = Get-Date + Write-Host "Processing Network enrichment ($($arrayVNets.Count) Virtual Networks)" + + $htVNets = @{} + foreach ($vnet in $arrayVNets) { + $htVNets.($vnet.id) = $vnet + } + + $script:htSubnets = @{} + $script:arrayVirtualNetworks = [System.Collections.ArrayList]@() + $script:arraySubnets = [System.Collections.ArrayList]@() + + foreach ($vnet in $arrayVNets) { + + #region peerings + $vnetIdSplit = ($vnet.id -split '/') + $subscriptionId = $vnetIdSplit[2] + + $subscriptionName = 'n/a' + $MGPath = 'n/a' + if ($htSubscriptionsMgPath.($subscriptionId)) { + $subHelper = $htSubscriptionsMgPath.($subscriptionId) + $subscriptionName = $subHelper.displayName + $MGPath = $subHelper.ParentNameChainDelimited + } + + $subnetsWithPrivateEndPointsCount = 0 + if ($vnet.properties.subnets.properties.privateEndpoints.id.Count -gt 0) { + $subnetsWithPrivateEndPointsCount = $vnet.properties.subnets.where({ $_.properties.privateEndpoints.id.Count -gt 0 }).Count + } + + $subnetsWithConnectedDevicesCount = 0 + if ($vnet.properties.subnets.properties.ipConfigurations.id.Count -gt 0) { + $subnetsWithConnectedDevicesCount = $vnet.properties.subnets.where({ $_.properties.ipConfigurations.id.Count -gt 0 }).Count + } + + $vnetResourceGroup = $vnetIdSplit[4] + if ($vnet.properties.virtualNetworkPeerings.id.Count -gt 0) { + foreach ($peering in $vnet.properties.virtualNetworkPeerings) { + $remotevnetIdSplit = ($peering.properties.remoteVirtualNetwork.id -split '/') + $remotesubscriptionId = $remotevnetIdSplit[2] + + $remotesubscriptionName = 'n/a' + $remoteMGPath = 'n/a' + $peeringXTenant = 'unknown' + if ($htSubscriptionsMgPath.($remotesubscriptionId)) { + $peeringXTenant = 'false' + $remotesubHelper = $htSubscriptionsMgPath.($remotesubscriptionId) + $remotesubscriptionName = $remotesubHelper.displayName + $remoteMGPath = $remotesubHelper.ParentNameChainDelimited + } + else { + if ($htUnknownTenantsForSubscription.($remotesubscriptionId)) { + $remoteTenantId = $htUnknownTenantsForSubscription.($remotesubscriptionId).TenantId + $remoteMGPath = $remoteTenantId + if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { + $peeringXTenant = 'false' + } + else { + $peeringXTenant = 'true' + } + } + else { + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($remotesubscriptionId)?api-version=2020-01-01" + $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($remotesubscriptionId)'" + if ($remoteTenantId.id -like '/subscriptions/*') { + #sub actually could be resolved but not available in htSubscriptionsMgPath + Write-Host "SubscriptionId '$($remotesubscriptionId)' (tenantId: '$($remoteTenantId.tenantId)' (current context tenantId: '$($azapiCallConf['checkContext'].tenant.Id)')) was not captured by getSubscriptions/getEntities, however could be fully resolved with direct get call (ARM subscription API)" -ForegroundColor Magenta + $remoteMGPath = $remoteTenantId.tenantId + if ($azapiCallConf['checkContext'].tenant.Id -eq $remoteTenantId.tenantId) { + $peeringXTenant = 'false' + } + else { + $peeringXTenant = 'true' + } + } + else { + $arrayRemoteMGPath = @() + foreach ($remoteId in $remoteTenantId) { + if ($remoteId -eq 'SubscriptionNotFound Tenant unknown') { + $remoteMGPath = 'unknown' + $peeringXTenant = 'n/a' + } + else { + $objectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { + if ($remoteId -in $MSTenantIds) { + $arrayRemoteMGPath += "$remoteId (MS)" + } + else { + $arrayRemoteMGPath += $remoteId + } + if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { + $peeringXTenant = 'false' + } + else { + $peeringXTenant = 'true' + } + } + $script:htUnknownTenantsForSubscription.($remotesubscriptionId) = @{} + $script:htUnknownTenantsForSubscription.($remotesubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' + $remoteMGPath += $arrayRemoteMGPath -join ', ' + } + } + } + } + } + + $remotevnetName = $remotevnetIdSplit[8] + $remotevnetResourceGroup = $remotevnetIdSplit[4] + + if ($htVNets.($peering.properties.remoteVirtualNetwork.id)) { + $remotevnetState = 'existent' + $remoteLocation = $htVNets.($peering.properties.remoteVirtualNetwork.id).location + $remotePeeringsCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.virtualNetworkPeerings.id.Count + $remoteDhcpoptionsDnsservers = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.dhcpoptions.dnsservers + $remoteSubnetsCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.id.Count + $remoteSubnetsWithNSGCount = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.networkSecurityGroup.id.Count + $remoteSubnetsWithRouteTable = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.routeTable.id.Count + $remoteSubnetsWithDelegations = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.delegations.id.Count + $remotePrivateEndPoints = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.privateEndpoints.id.Count + $remoteSubnetsWithPrivateEndPoints = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.where({ $_.properties.privateEndpoints.id.Count -gt 0 }).Count + $remoteConnectedDevices = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.properties.ipConfigurations.id.Count + $remoteSubnetsWithConnectedDevices = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.subnets.where({ $_.properties.ipConfigurations.id.Count -gt 0 }).Count + $remoteDdosProtection = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.enableDdosProtection + $remotePeering = $htVNets.($peering.properties.remoteVirtualNetwork.id).properties.virtualNetworkPeerings.where({ $_.properties.remoteVirtualNetwork.id -eq $vnet.id }) + if ($remotePeering.count -eq 1) { + $remotePeeringName = $remotePeering.name + $remotePeeringState = $remotePeering.Properties.peeringState + $remotePeeringSyncLevel = $remotePeering.Properties.peeringSyncLevel + $remoteAllowVirtualNetworkAccess = $remotePeering.properties.allowVirtualNetworkAccess + $remoteAllowForwardedTraffic = $remotePeering.properties.allowForwardedTraffic + $remoteAllowGatewayTransit = $remotePeering.properties.allowGatewayTransit + $remoteUseRemoteGateways = $remotePeering.properties.useRemoteGateways + $remoteDoNotVerifyRemoteGateways = $remotePeering.properties.doNotVerifyRemoteGateways + $remotePeerCompleteVnets = $remotePeering.properties.peerCompleteVnets + $remoteRouteServiceVips = $remotePeering.properties.routeServiceVips + } + else { + $remotePeeringName = 'n/a' + $remotePeeringState = 'n/a' + $remotePeeringSyncLevel = 'n/a' + $remoteAllowVirtualNetworkAccess = 'n/a' + $remoteAllowForwardedTraffic = 'n/a' + $remoteAllowGatewayTransit = 'n/a' + $remoteUseRemoteGateways = 'n/a' + $remoteDoNotVerifyRemoteGateways = 'n/a' + $remotePeerCompleteVnets = 'n/a' + $remoteRouteServiceVips = 'n/a' + } + + } + else { + if ($getMgParentName -eq 'Tenant Root') { + $remotevnetState = 'non-existent' + } + else { + $remotevnetState = 'n/a' + } + $remoteLocation = 'n/a' + $remotePeeringsCount = 'n/a' + $remoteDhcpoptionsDnsservers = 'n/a' + $remoteSubnetsCount = 'n/a' + $remoteSubnetsWithNSGCount = 'n/a' + $remoteSubnetsWithRouteTable = 'n/a' + $remoteSubnetsWithDelegations = 'n/a' + $remotePrivateEndPoints = 'n/a' + $remoteSubnetsWithPrivateEndPoints = 'n/a' + $remoteConnectedDevices = 'n/a' + $remoteSubnetsWithConnectedDevices = 'n/a' + $remoteDdosProtection = 'n/a' + $remotePeeringName = 'n/a' + $remotePeeringState = 'n/a' + $remotePeeringSyncLevel = 'n/a' + $remoteAllowVirtualNetworkAccess = 'n/a' + $remoteAllowForwardedTraffic = 'n/a' + $remoteAllowGatewayTransit = 'n/a' + $remoteUseRemoteGateways = 'n/a' + $remoteDoNotVerifyRemoteGateways = 'n/a' + $remotePeerCompleteVnets = 'n/a' + $remoteRouteServiceVips = 'n/a' + } + + $null = $script:arrayVirtualNetworks.Add([PSCustomObject]@{ + SubscriptionName = $subscriptionName + Subscription = ($vnet.id -split '/')[2] + MGPath = $MGPath + VNet = $vnet.name + VNetId = $vnet.id + VNetResourceGroup = $vnetResourceGroup + Location = $vnet.location + AddressSpaceAddressPrefixes = ($vnet.properties.addressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") + DhcpoptionsDnsservers = ($vnet.properties.dhcpoptions.dnsservers -join "$CsvDelimiterOpposite ") + SubnetsCount = $vnet.properties.subnets.id.Count + SubnetsWithNSGCount = $vnet.properties.subnets.properties.networkSecurityGroup.id.Count + SubnetsWithRouteTableCount = $vnet.properties.subnets.properties.routeTable.id.Count + SubnetsWithDelegationsCount = $vnet.properties.subnets.properties.delegations.id.Count + PrivateEndpointsCount = $vnet.properties.subnets.properties.privateEndpoints.id.Count + SubnetsWithPrivateEndPointsCount = $subnetsWithPrivateEndPointsCount + ConnectedDevices = $vnet.properties.subnets.properties.ipConfigurations.id.Count + SubnetsWithConnectedDevicesCount = $subnetsWithConnectedDevicesCount + DdosProtection = $vnet.properties.enableDdosProtection + + PeeringsCount = $vnet.properties.virtualNetworkPeerings.id.Count + PeeringXTenant = $peeringXTenant + PeeringName = $peering.name + PeeringState = $peering.properties.peeringState + PeeringSyncLevel = $peering.properties.peeringSyncLevel + AllowVirtualNetworkAccess = $peering.properties.allowVirtualNetworkAccess + AllowForwardedTraffic = $peering.properties.allowForwardedTraffic + AllowGatewayTransit = $peering.properties.allowGatewayTransit + UseRemoteGateways = $peering.properties.useRemoteGateways + DoNotVerifyRemoteGateways = $peering.properties.doNotVerifyRemoteGateways + PeerCompleteVnets = $peering.properties.peerCompleteVnets + RouteServiceVips = $peering.properties.routeServiceVips + + RemotePeeringsCount = $remotePeeringsCount + RemotePeeringName = $remotePeeringName + RemotePeeringState = $remotePeeringState + RemotePeeringSyncLevel = $remotePeeringSyncLevel + RemoteAllowVirtualNetworkAccess = $RemoteAllowVirtualNetworkAccess + RemoteAllowForwardedTraffic = $RemoteAllowForwardedTraffic + RemoteAllowGatewayTransit = $RemoteAllowGatewayTransit + RemoteUseRemoteGateways = $RemoteUseRemoteGateways + RemoteDoNotVerifyRemoteGateways = $RemoteDoNotVerifyRemoteGateways + RemotePeerCompleteVnets = $RemotePeerCompleteVnets + RemoteRouteServiceVips = $RemoteRouteServiceVips + + RemoteSubscriptionName = $remotesubscriptionName + RemoteSubscription = $remotesubscriptionId + RemoteMGPath = $remoteMGPath -join ', ' + RemoteVNet = $remotevnetName + RemoteVNetId = $peering.properties.remoteVirtualNetwork.id + RemoteVNetState = $remotevnetState + RemoteVNetResourceGroup = $remotevnetResourceGroup + RemoteVNetLocation = $remoteLocation + RemoteAddressSpaceAddressPrefixes = ($peering.properties.remoteAddressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") + RemoteVirtualNetworkAddressSpaceAddressPrefixes = ($peering.properties.remoteVirtualNetworkAddressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") + + RemoteDhcpoptionsDnsservers = ($remoteDhcpoptionsDnsservers -join "$CsvDelimiterOpposite ") + RemoteSubnetsCount = $remoteSubnetsCount + RemoteSubnetsWithNSGCount = $remoteSubnetsWithNSGCount + RemoteSubnetsWithRouteTable = $remoteSubnetsWithRouteTable + RemoteSubnetsWithDelegations = $remoteSubnetsWithDelegations + RemotePrivateEndPoints = $remotePrivateEndPoints + RemoteSubnetsWithPrivateEndPoints = $remoteSubnetsWithPrivateEndPoints + RemoteConnectedDevices = $remoteConnectedDevices + RemoteSubnetsWithConnectedDevices = $remoteSubnetsWithConnectedDevices + RemoteDdosProtection = $remoteDdosProtection + }) + } + + } + else { + $null = $script:arrayVirtualNetworks.Add([PSCustomObject]@{ + SubscriptionName = $subscriptionName + Subscription = ($vnet.id -split '/')[2] + MGPath = $MGPath + VNet = $vnet.name + VNetId = $vnet.id + VNetResourceGroup = $vnetResourceGroup + Location = $vnet.location + + AddressSpaceAddressPrefixes = ($vnet.properties.addressSpace.addressPrefixes -join "$CsvDelimiterOpposite ") + DhcpoptionsDnsservers = ($vnet.properties.dhcpoptions.dnsservers -join "$CsvDelimiterOpposite ") + SubnetsCount = $vnet.properties.subnets.id.Count + SubnetsWithNSGCount = $vnet.properties.subnets.properties.networkSecurityGroup.id.Count + SubnetsWithRouteTableCount = $vnet.properties.subnets.properties.routeTable.id.Count + SubnetsWithDelegationsCount = $vnet.properties.subnets.properties.delegations.id.Count + PrivateEndpointsCount = $vnet.properties.subnets.properties.privateEndpoints.id.Count + SubnetsWithPrivateEndPointsCount = $subnetsWithPrivateEndPointsCount + ConnectedDevices = $vnet.properties.subnets.properties.ipConfigurations.id.Count + SubnetsWithConnectedDevicesCount = $subnetsWithConnectedDevicesCount + DdosProtection = $vnet.properties.enableDdosProtection + + PeeringsCount = $vnet.properties.virtualNetworkPeerings.id.Count + PeeringXTenant = 'n/a' + PeeringName = '' + PeeringState = '' + PeeringSyncLevel = '' + AllowVirtualNetworkAccess = '' + AllowForwardedTraffic = '' + AllowGatewayTransit = '' + UseRemoteGateways = '' + DoNotVerifyRemoteGateways = '' + PeerCompleteVnets = '' + RouteServiceVips = '' + + RemotePeeringsCount = '' + RemotePeeringName = '' + RemotePeeringState = '' + RemotePeeringSyncLevel = '' + RemoteAllowVirtualNetworkAccess = '' + RemoteAllowForwardedTraffic = '' + RemoteAllowGatewayTransit = '' + RemoteUseRemoteGateways = '' + RemoteDoNotVerifyRemoteGateways = '' + RemotePeerCompleteVnets = '' + RemoteRouteServiceVips = '' + + RemoteSubscriptionName = '' + RemoteSubscription = '' + RemoteMGPath = '' + RemoteVNet = '' + RemoteVNetId = '' + RemoteVNetState = '' + RemoteVNetResourceGroup = '' + RemoteVNetLocation = '' + RemoteAddressSpaceAddressPrefixes = '' + RemoteVirtualNetworkAddressSpaceAddressPrefixes = '' + RemoteDhcpoptionsDnsservers = '' + RemoteSubnetsCount = '' + RemoteSubnetsWithNSGCount = '' + RemoteSubnetsWithRouteTable = '' + RemoteSubnetsWithDelegations = '' + RemotePrivateEndPoints = '' + RemoteSubnetsWithPrivateEndPoints = '' + RemoteConnectedDevices = '' + RemoteSubnetsWithConnectedDevices = '' + RemoteDdosProtection = '' + }) + } + #endregion peerings + + #region subnets + + if ($vnet.properties.subnets.Count -gt 0) { + foreach ($subnet in $vnet.properties.subnets) { + + $script:htSubnets.($subnet.id) = @{ + SubscriptionName = $subscriptionName + Subscription = ($vnet.id -split '/')[2] + MGPath = $MGPath + VNet = $vnet.name + VNetId = $vnet.id + Location = $vnet.location + ResourceGroup = $vnetResourceGroup + } + + $arrayServiceEndPoints = @() + if ($subnet.properties.serviceEndpoints.service.Count -gt 0) { + $arrayServiceEndPoints = foreach ($serviceEndpoint in $subnet.properties.serviceEndpoints) { + "$($serviceEndpoint.service) ($(($serviceEndpoint.locations | Sort-Object) -join ', '))" + } + } + + $delegation = '' + if ($subnet.properties.delegations.Count -gt 0) { + $delegation = "$($subnet.properties.delegations.properties.serviceName) ($(($subnet.properties.delegations.properties.actions | Sort-Object) -join ', '))" + } + + #region IP address usage + #https://github.com/ElanShudnow/AzureCode/blob/242b923eada55fa795b930473a50dedf14bdc409/PowerShell/AzSubnetAvailability/AzSubnetAvailability.ps1 + # Gets the mask from the IP configuration (I.e 10.0.0.0/24, turns to just "24") + + if (-not [string]::IsNullOrWhiteSpace($subnet.properties.addressPrefix)) { + $AddressPrefix = $subnet.properties.addressPrefix + $subnetNet = $AddressPrefix -replace '/.*' + $subnetNetOutput = $subnetNet + } + + #ignore IPv6 + if (-not [string]::IsNullOrWhiteSpace($subnet.properties.addressPrefixes)) { + $arr = foreach ($entry in $subnet.properties.addressPrefixes) { + if ($entry -match '^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])\/(\d{1}|[0-2]{1}\d{1}|3[0-2])$') { + $AddressPrefix = $entry + $AddressPrefix -replace '/.*' + $subnetNet = $AddressPrefix -replace '/.*' + } + else { + "(ignoring IPv6 $entry)" + } + } + $subnetNetOutput = $arr + } + + $Mask = $AddressPrefix.substring($AddressPrefix.Length - 2, 2) + + #Amount of available IP Addresses minus the 3 IPs that Azure consumes, minus net and broadcast + #https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets + switch ($Mask) { + '30' { $AvailableAddresses = [Math]::Pow(2, 2) - 5 } + '29' { $AvailableAddresses = [Math]::Pow(2, 3) - 5 } + '28' { $AvailableAddresses = [Math]::Pow(2, 4) - 5 } + '27' { $AvailableAddresses = [Math]::Pow(2, 5) - 5 } + '26' { $AvailableAddresses = [Math]::Pow(2, 6) - 5 } + '25' { $AvailableAddresses = [Math]::Pow(2, 7) - 5 } + '24' { $AvailableAddresses = [Math]::Pow(2, 8) - 5 } + '23' { $AvailableAddresses = [Math]::Pow(2, 9) - 5 } + '22' { $AvailableAddresses = [Math]::Pow(2, 10) - 5 } + '21' { $AvailableAddresses = [Math]::Pow(2, 11) - 5 } + '20' { $AvailableAddresses = [Math]::Pow(2, 12) - 5 } + '19' { $AvailableAddresses = [Math]::Pow(2, 13) - 5 } + '18' { $AvailableAddresses = [Math]::Pow(2, 14) - 5 } + '17' { $AvailableAddresses = [Math]::Pow(2, 15) - 5 } + '16' { $AvailableAddresses = [Math]::Pow(2, 16) - 5 } + '15' { $AvailableAddresses = [Math]::Pow(2, 17) - 5 } + '14' { $AvailableAddresses = [Math]::Pow(2, 18) - 5 } + '13' { $AvailableAddresses = [Math]::Pow(2, 19) - 5 } + '12' { $AvailableAddresses = [Math]::Pow(2, 20) - 5 } + '11' { $AvailableAddresses = [Math]::Pow(2, 21) - 5 } + '10' { $AvailableAddresses = [Math]::Pow(2, 22) - 5 } + '9' { $AvailableAddresses = [Math]::Pow(2, 23) - 5 } + '8' { $AvailableAddresses = [Math]::Pow(2, 24) - 5 } + } + + $IPsLeft = $AvailableAddresses - $subnet.properties.ipConfigurations.Count + $PercentIPsUsed = [math]::Round((($subnet.properties.ipConfigurations.Count / $AvailableAddresses) * 100), 1) + $subnetIPAddressUsageCritical = $false + if ($PercentIPsUsed -gt $NetworkSubnetIPAddressUsageCriticalPercentage) { + $subnetIPAddressUsageCritical = $true + } + + #endregion IP address usage + + $subnetPrefix = $AddressPrefix -replace '.*/' + + $subnetmask = ([IPAddress]"$([system.convert]::ToInt64(('1'*$subnetPrefix).PadRight(32,'0'),2))").IPAddressToString + $IPBits = [int[]]$subnetNet.Split('.') + $MaskBits = [int[]]$subnetmask.Split('.') + $NetworkIDBits = 0..3 | ForEach-Object { $IPBits[$_] -band $MaskBits[$_] } + $Broadcast = (0..3 | ForEach-Object { $NetworkIDBits[$_] + ($MaskBits[$_] -bxor 255) }) -join '.' + $Range = "$subnetNet - $Broadcast" + + $null = $script:arraySubnets.Add([PSCustomObject]@{ + SubscriptionName = $subscriptionName + Subscription = ($vnet.id -split '/')[2] + MGPath = $MGPath + VNet = $vnet.name + VNetId = $vnet.id + VNetResourceGroup = $vnetResourceGroup + Location = $vnet.location + SubnetName = $subnet.name + SubnetId = $subnet.id + SubnetNet = $subnetNetOutput -join "$CsvDelimiterOpposite " + SubnetPrefix = $subnetPrefix + Subnetmask = $subnetmask + Range = $Range + ConnectedDevices = $subnet.properties.ipConfigurations.Count + AvailableIPAddresses = $IPsLeft + UsedIPAddressesPercent = "$PercentIPsUsed %" + SubnetIPAddressUsageCritical = $subnetIPAddressUsageCritical + PrivateEndpointNetworkPolicies = $subnet.properties.privateEndpointNetworkPolicies + PrivateLinkServiceNetworkPolicies = $subnet.properties.privateLinkServiceNetworkPolicies + ServiceEndpointsCount = $subnet.properties.serviceEndpoints.service.Count + ServiceEndpoints = $arrayServiceEndPoints -join ', ' + Delegation = $delegation + NetworkSecurityGroup = $subnet.properties.networkSecurityGroup.id + RouteTable = $subnet.properties.routeTable + NatGateway = '' + PrivateEndpoints = $subnet.properties.privateEndpoints.Count + }) + } + } + #endregion subnets + } + + $end = Get-Date + Write-Host " Processing Network enrichment duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" +} +function processPrivateEndpoints { + $start = Get-Date + Write-Host 'Processing Private Endpoints enrichment' + + $script:arrayPrivateEndpointsEnriched = [System.Collections.ArrayList]@() + + if ($arrayPrivateEndPointsFromResourceProperties.Count -gt 0) { + $privateEndPointsFromResourcePropertiesToProcess = ($arrayPrivateEndPointsFromResourceProperties.where({ $arrayPrivateEndPoints.id -notcontains $_.privateEndpointConnection.Properties.privateEndpoint.id })) + $privateEndPointsFromResourcePropertiesToProcessCount = $privateEndPointsFromResourcePropertiesToProcess.Count + Write-Host " Processing Private Endpoints enrichment for $privateEndPointsFromResourcePropertiesToProcessCount Private Endpoint(s) where the Private Endpoint was not returned from the PE API endpoint but from a resource property" + if ($privateEndPointsFromResourcePropertiesToProcessCount -gt 0) { + foreach ($entry in $privateEndPointsFromResourcePropertiesToProcess) { + $peResIdSplit = $entry.privateEndpointConnection.Properties.privateEndpoint.id -split '/' + $crossSubscriptionPE = 'n/a' + $peSubscriptionId = $peResIdSplit[2] + if ($peSubscriptionId -ne $entry.ResourceSubscriptionId) { + $crossSubscriptionPE = $true + } + else { + $crossSubscriptionPE = $false + } + + $peMGPath = 'n/a' + $peXTenant = 'unknown' + if ($htSubscriptionsMgPath.($peSubscriptionId)) { + $peMGPath = $htSubscriptionsMgPath.($peSubscriptionId).pathDelimited + $peXTenant = $false + } + elseif ($htUnknownTenantsForSubscription.($peSubscriptionId)) { + $remoteTenantId = $htUnknownTenantsForSubscription.($peSubscriptionId).TenantId + $peMGPath = $remoteTenantId + if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { + $peXTenant = $false + } + else { + $peXTenant = $true + } + } + else { + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($peSubscriptionId)?api-version=2020-01-01" + $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($peSubscriptionId)'" + $arrayRemoteMGPath = @() + foreach ($remoteId in $remoteTenantId) { + $objectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { + if ($remoteId -in $MSTenantIds) { + $arrayRemoteMGPath += "$remoteId (MS)" + } + else { + $arrayRemoteMGPath += $remoteId + } + if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { + $peXTenant = $false + } + else { + $peXTenant = $true + } + } + $script:htUnknownTenantsForSubscription.($peSubscriptionId) = @{} + $script:htUnknownTenantsForSubscription.($peSubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' + $peMGPath = $arrayRemoteMGPath -join ', ' + } + } + + $null = $script:arrayPrivateEndpointsEnriched.Add([PSCustomObject]@{ + PEName = $entry.privateEndpointConnection.name + PEId = $entry.privateEndpointConnection.Properties.privateEndpoint.id + PELocation = 'n/a' + PEResourceGroup = $peResIdSplit[4] + PESubscriptionName = 'n/a' + PESubscription = $peSubscriptionId + PEMGPath = $peMGPath + PEConnectionType = 'n/a' + PEConnectionState = $entry.privateEndpointConnection.Properties.privateLinkServiceConnectionState.status + CrossSubscriptionPE = $crossSubscriptionPE + CrossTenantPE = $peXTenant + + Resource = $entry.ResourceName + ResourceType = $entry.ResourceType + ResourceId = $entry.ResourceId + TargetSubresource = 'n/a' + NICName = 'n/a' + FQDN = 'n/a' + ipAddresses = 'n/a' + ResourceResourceGroup = $entry.ResourceResourceGroup + ResourceSubscriptionName = $entry.ResourceSubscriptionName + ResourceSubscriptionId = $entry.ResourceSubscriptionId + ResourceMGPath = $entry.ResourceMGPath + ResourceCrossTenant = 'false' + + Subnet = 'n/a' + SubnetId = 'n/a' + SubnetVNet = 'n/a' + SubnetVNetId = 'n/a' + SubnetVNetLocation = 'n/a' + SubnetVNetResourceGroup = 'n/a' + SubnetSubscriptionName = 'n/a' + SubnetSubscription = 'n/a' + SubnetMGPath = 'n/a' + }) + } + } + } + + Write-Host " Processing Private Endpoints enrichment for $($arrayPrivateEndPoints.Count) Private Endpoint(s) where the Private Endpoint was returned from the PE API endpoint" + $htVPrivateEndPoints = @{} + foreach ($pe in $arrayPrivateEndPoints) { + $htVPrivateEndPoints.($pe.id) = $pe + } + + $htVPrivateEndPoints = @{} + foreach ($pe in $arrayPrivateEndPoints) { + $htVPrivateEndPoints.($pe.id) = $pe + } + + foreach ($pe in $arrayPrivateEndPoints) { + + $peIdSplit = ($pe.id -split '/') + $subscriptionId = $peIdSplit[2] + $resourceGroup = $peIdSplit[4] + + $subscriptionName = 'n/a' + $MGPath = 'n/a' + if ($htSubscriptionsMgPath.($subscriptionId)) { + $subHelper = $htSubscriptionsMgPath.($subscriptionId) + $subscriptionName = $subHelper.displayName + $MGPath = $subHelper.ParentNameChainDelimited + } + + $SubnetSubscriptionName = 'n/a' + $SubnetSubscription = 'n/a' + $SubnetMGPath = 'n/a' + $SubnetVNet = 'n/a' + $SubnetVNetId = 'n/a' + $SubnetVNetLocation = 'n/a' + $SubnetVNetResourceGroup = 'n/a' + if ($htSubnets.($pe.properties.subnet.id)) { + $hlper = $htSubnets.($pe.properties.subnet.id) + $SubnetSubscriptionName = $hlper.SubscriptionName + $SubnetSubscription = $hlper.Subscription + $SubnetMGPath = $hlper.MGPath + $SubnetVNet = $hlper.VNet + $SubnetVNetId = $hlper.VNetId + $SubnetVNetLocation = $hlper.Location + $SubnetVNetResourceGroup = $hlper.ResourceGroup + } + + $resourceSplit = $false + if ($pe.properties.privateLinkServiceConnections.Count -gt 0) { + $resourceId = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceId + $targetSubresource = $pe.properties.privateLinkServiceConnections.properties.groupIds -join ', ' + $resourceSplit = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceId -split '/' + $peConnectionType = 'direct' + $peConnectionState = $pe.properties.privateLinkServiceConnections.properties.privateLinkServiceConnectionState.status + } + if ($pe.properties.manualPrivateLinkServiceConnections.Count -gt 0) { + $resourceId = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceId + $targetSubresource = $pe.properties.manualPrivateLinkServiceConnections.properties.groupIds -join ', ' + $resourceSplit = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceId -split '/' + $peConnectionType = 'manual' + $peConnectionState = $pe.properties.manualPrivateLinkServiceConnections.properties.privateLinkServiceConnectionState.status + } + + $resourceSubscriptionId = 'n/a' + $resource = 'n/a' + $resourceType = 'n/a' + $resourceResourceGroup = 'n/a' + $resourceSubscriptionName = 'n/a' + $resourceMGPath = 'n/a' + $crossSubscriptionPE = 'n/a' + $resourceXTenant = 'unknown' + + if ($resourceSplit) { + $ObjectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($resourceSplit[2], [System.Management.Automation.PSReference]$ObjectGuid)) { + $resourceSubscriptionId = $resourceSplit[2] + $resource = $resourceSplit[8] + $resourceType = "$($resourceSplit[6])/$($resourceSplit[7])" + $resourceResourceGroup = $resourceSplit[4] + + if ($htSubscriptionsMgPath.($resourceSubscriptionId)) { + $subHelper = $htSubscriptionsMgPath.($resourceSubscriptionId) + $resourceSubscriptionName = $subHelper.displayName + $resourceMGPath = $subHelper.ParentNameChainDelimited + $resourceXTenant = $false + } + else { + if ($htUnknownTenantsForSubscription.($resourceSubscriptionId)) { + $remoteTenantId = $htUnknownTenantsForSubscription.($resourceSubscriptionId).TenantId + $resourceMGPath = $remoteTenantId + if ($remoteTenantId -eq $azApiCallConf['checkcontext'].tenant.id) { + $resourceXTenant = $false + } + else { + $resourceXTenant = $true + } + } + else { + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($resourceSubscriptionId)?api-version=2020-01-01" + $remoteTenantId = AzAPICall -AzAPICallConfiguration $azApiCallConf -uri $uri -listenOn 'content' -currentTask "getTenantId for subscriptionId '$($resourceSubscriptionId)'" + $arrayRemoteMGPath = @() + foreach ($remoteId in $remoteTenantId) { + $objectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($remoteId, [System.Management.Automation.PSReference]$ObjectGuid)) { + if ($remoteId -in $MSTenantIds) { + $arrayRemoteMGPath += "$remoteId (MS)" + } + else { + $arrayRemoteMGPath += $remoteId + } + if ($remoteId -eq $azApiCallConf['checkcontext'].tenant.id) { + $resourceXTenant = $false + } + else { + $resourceXTenant = $true + } + } + $script:htUnknownTenantsForSubscription.($resourceSubscriptionId) = @{} + $script:htUnknownTenantsForSubscription.($resourceSubscriptionId).TenantId = $arrayRemoteMGPath -join ', ' + $resourceMGPath = $arrayRemoteMGPath -join ', ' + } + } + } + + if ($SubnetSubscription -eq $resourceSubscriptionId) { + $crossSubscriptionPE = $false + } + else { + $crossSubscriptionPE = $true + } + + $crossTenantPE = $false + if ($resourceXTenant -eq $true) { + $crossTenantPE = $true + } + + } + } + + $null = $script:arrayPrivateEndpointsEnriched.Add([PSCustomObject]@{ + PEName = $pe.name + PEId = $pe.id + PELocation = $pe.location + PEResourceGroup = $resourceGroup + PESubscriptionName = $subscriptionName + PESubscription = ($pe.id -split '/')[2] + PEMGPath = $MGPath + PEConnectionType = $peConnectionType + PEConnectionState = $peConnectionState + CrossSubscriptionPE = $crossSubscriptionPE + CrossTenantPE = $crossTenantPE + + Resource = $resource + ResourceType = $resourceType + ResourceId = $resourceId + TargetSubresource = $targetSubresource -join ', ' + NICName = $pe.properties.customNetworkInterfaceName + FQDN = $pe.properties.customDnsConfigs.fqdn -join ', ' + ipAddresses = $pe.properties.customDnsConfigs.ipAddresses -join ', ' + ResourceResourceGroup = $resourceResourceGroup + ResourceSubscriptionName = $resourceSubscriptionName + ResourceSubscriptionId = $resourceSubscriptionId + ResourceMGPath = $resourceMGPath + ResourceCrossTenant = $resourceXTenant + + Subnet = $pe.properties.subnet.id -replace '.*/' + SubnetId = $pe.properties.subnet.id + SubnetVNet = $SubnetVNet + SubnetVNetId = $SubnetVNetId + SubnetVNetLocation = $SubnetVNetLocation + SubnetVNetResourceGroup = $SubnetVNetResourceGroup + SubnetSubscriptionName = $SubnetSubscriptionName + SubnetSubscription = $SubnetSubscription + SubnetMGPath = $SubnetMGPath + }) + } + + + $end = Get-Date + Write-Host " Processing Private Endpoints enrichment duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" +} +function processScopeInsightsMgOrSub($mgOrSub, $mgChild, $subscriptionId, $subscriptionsMgId) { + $script:scopescnter++ + $htmlScopeInsights = $null + $htmlScopeInsights = [System.Text.StringBuilder]::new() + #region ScopeInsightsBaseCollection + if ($mgOrSub -eq 'mg') { + #$startScopeInsightsPreQueryMg = Get-Date + #BLUEPRINT + $blueprintReleatedQuery = $blueprintBaseQuery.where( { $_.MgId -eq $mgChild -and [String]::IsNullOrEmpty($_.SubscriptionId) -and [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) + $blueprintsScoped = $blueprintReleatedQuery + $blueprintsScopedCount = ($blueprintsScoped).count + #Resources + $mgAllChildSubscriptions = [System.Collections.ArrayList]@() + $mgAllChildSubscriptions = foreach ($entry in $htSubscriptionsMgPath.keys) { + if (($htSubscriptionsMgPath.($entry).ParentNameChain) -contains $mgchild) { + $entry + } + } + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + $resourcesAllChildSubscriptions = [System.Collections.ArrayList]@() + foreach ($mgAllChildSubscription in $mgAllChildSubscriptions) { + foreach ($resource in ($resourcesAllGroupedBySubcriptionId.where( { $_.name -eq $mgAllChildSubscription } )).group | Sort-Object -Property type, location) { + $null = $resourcesAllChildSubscriptions.Add($resource) + } + + } + $resourcesAllChildSubscriptionsArray = [System.Collections.ArrayList]@() + $grp = $resourcesAllChildSubscriptions | Group-Object -Property type, location + foreach ($resLoc in $grp) { + $cnt = 0 + $ResoureTypeAndLocation = $resLoc.Name.split(',') + $resLoc.Group.count_ | ForEach-Object { $cnt += $_ } + $null = $resourcesAllChildSubscriptionsArray.Add([PSCustomObject]@{ + ResourceType = $ResoureTypeAndLocation[0] + Location = $ResoureTypeAndLocation[1] + ResourceCount = $cnt + }) + } + $resourcesAllChildSubscriptions.count_ | ForEach-Object { $resourcesAllChildSubscriptionTotal += $_ } + $resourcesAllChildSubscriptionResourceTypeCount = (($resourcesAllChildSubscriptions | Sort-Object -Property type -Unique) | Measure-Object).count + $resourcesAllChildSubscriptionLocationCount = (($resourcesAllChildSubscriptions | Sort-Object -Property location -Unique) | Measure-Object).count + } + #childrenMgInfo + $mgAllChildMgs = [System.Collections.ArrayList]@() + $mgAllChildMgs = foreach ($entry in $htManagementGroupsMgPath.keys) { + if (($htManagementGroupsMgPath.($entry).path) -contains $mgchild) { + $entry + } + } + + $arrayPolicyAssignmentsEnrichedForThisManagementGroup = ($arrayPolicyAssignmentsEnrichedGroupedByManagementGroup.where( { $_.name -eq $mgChild } )).group + $arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant = $arrayPolicyAssignmentsEnrichedForThisManagementGroup | Group-Object -Property PolicyVariant + $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicy = ($arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant.where( { $_.name -eq 'Policy' } )).group + $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicySet = ($arrayPolicyAssignmentsEnrichedForThisManagementGroupGroupedByPolicyVariant.where( { $_.name -eq 'PolicySet' } )).group + + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { + if ([string]::IsNullOrEmpty(($htMgASCSecureScore).($mgChild).SecureScore) -or [string]::IsNullOrWhiteSpace(($htMgASCSecureScore).($mgChild).SecureScore)) { + $managementGroupASCPoints = 'n/a' + } + else { + $managementGroupASCPoints = ($htMgASCSecureScore).($mgChild).SecureScore + } + } + else { + $managementGroupASCPoints = "excluded (-NoMDfCSecureScore $($azAPICallConf['htParameters'].NoMDfCSecureScore))" + } + + $cssClass = 'mgDetailsTable' + + #$endScopeInsightsPreQueryMg = Get-Date + #Write-Host " ScopeInsights MG PreQuery processing duration: $((NEW-TIMESPAN -Start $startScopeInsightsPreQueryMg -End $endScopeInsightsPreQueryMg).TotalSeconds) seconds" + } + if ($mgOrSub -eq 'sub') { + #$startScopeInsightsPreQuerySub = Get-Date + #BLUEPRINT + $blueprintReleatedQuery = $blueprintBaseQuery.where( { $_.SubscriptionId -eq $subscriptionId -and -not [String]::IsNullOrEmpty($_.BlueprintName) } ) + $blueprintsAssigned = $blueprintReleatedQuery.where( { -not [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) + $blueprintsAssignedCount = ($blueprintsAssigned).count + $blueprintsScoped = $blueprintReleatedQuery.where( { $_.BlueprintScoped -eq "/subscriptions/$subscriptionId" -and [String]::IsNullOrEmpty($_.BlueprintAssignmentId) } ) + $blueprintsScopedCount = ($blueprintsScoped).count + #SubscriptionDetails + $subPath = $htSubscriptionsMgPath.($subscriptionId).pathDelimited + $subscriptionDetailsReleatedQuery = $htSubDetails.($subscriptionId).details + $subscriptionState = ($subscriptionDetailsReleatedQuery).SubscriptionState + $subscriptionQuotaId = ($subscriptionDetailsReleatedQuery).SubscriptionQuotaId + $subscriptionResourceGroupsCount = ($resourceGroupsAll.where( { $_.subscriptionId -eq $subscriptionId } )).count_ + if (-not $subscriptionResourceGroupsCount) { + $subscriptionResourceGroupsCount = 0 + } + $subscriptionASCPoints = ($subscriptionDetailsReleatedQuery).SubscriptionASCSecureScore + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #Resources + $resourcesSubscription = [System.Collections.ArrayList]@() + foreach ($resource in ($resourcesAllGroupedBySubcriptionId.where( { $_.name -eq $subscriptionId } )).group | Sort-Object -Property type, location) { + $null = $resourcesSubscription.Add($resource) + } + + $resourcesSubscriptionTotal = 0 + $resourcesSubscription.count_ | ForEach-Object { $resourcesSubscriptionTotal += $_ } + $resourcesSubscriptionResourceTypeCount = (($resourcesSubscription | Sort-Object -Property type -Unique)).count + $resourcesSubscriptionLocationCount = (($resourcesSubscription | Sort-Object -Property location -Unique)).count + } + + $arrayPolicyAssignmentsEnrichedForThisSubscription = ($arrayPolicyAssignmentsEnrichedGroupedBySubscription.where( { $_.name -eq $subscriptionId } )).group + $arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant = $arrayPolicyAssignmentsEnrichedForThisSubscription | Group-Object -Property PolicyVariant + $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicy = ($arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant.where( { $_.name -eq 'Policy' } )).group + $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicySet = ($arrayPolicyAssignmentsEnrichedForThisSubscriptionGroupedByPolicyVariant.where( { $_.name -eq 'PolicySet' } )).group + + $arrayDefenderPlansSubscription = $defenderPlansGroupedBySub.where( { $_.Name -like "*$($subscriptionId)*" } ) + + $arrayUserAssignedIdentities4ResourcesSubscription = $arrayUserAssignedIdentities4Resources.where( { $_.resourceSubscriptionId -eq $subscriptionId -or $_.miSubscriptionId -eq $subscriptionId } ) + $arrayUserAssignedIdentities4ResourcesSubscriptionCount = $arrayUserAssignedIdentities4ResourcesSubscription.Count + + if ($subFeaturesGroupedBySubscription) { + $subscriptionFeatures = $subFeaturesGroupedBySubscription.where({ $_.name -eq $subscriptionId }) + } + + $cssClass = 'subDetailsTable' + + #$endScopeInsightsPreQuerySub = Get-Date + #Write-Host " ScopeInsights SUB PreQuery processing duration: $((NEW-TIMESPAN -Start $startScopeInsightsPreQuerySub -End $endScopeInsightsPreQuerySub).TotalSeconds) seconds" + } + #endregion ScopeInsightsBaseCollection + + if ($mgOrSub -eq 'sub') { + + if ($htDefenderEmailContacts.($subscriptionDetailsReleatedQuery.subscriptionId)) { + $hlpDefenderEmailContacts = $htDefenderEmailContacts.($subscriptionDetailsReleatedQuery.subscriptionId) + $MDfCEmailNotificationsState = $hlpDefenderEmailContacts.alertNotificationsState + $MDfCEmailNotificationsSeverity = $hlpDefenderEmailContacts.alertNotificationsminimalSeverity + $MDfCEmailNotificationsRoles = $hlpDefenderEmailContacts.roles + $MDfCEmailNotificationsEmails = $hlpDefenderEmailContacts.emails + } + else { + $MDfCEmailNotificationsState = '' + $MDfCEmailNotificationsSeverity = '' + $MDfCEmailNotificationsRoles = '' + $MDfCEmailNotificationsEmails = '' + } + + [void]$htmlScopeInsights.AppendLine(@" +Subscription Name: $($subscriptionDetailsReleatedQuery.subscription -replace '<', '<' -replace '>', '>') +Subscription Id: $($subscriptionDetailsReleatedQuery.subscriptionId) +Subscription Path: $subPath +State: $subscriptionState +QuotaId: $subscriptionQuotaId + Microsoft Defender for Cloud Secure Score: $subscriptionASCPoints Video , Blog , docs + Microsoft Defender for Cloud 'Email notifications' state: $MDfCEmailNotificationsState + Microsoft Defender for Cloud 'Email notifications' severity: $MDfCEmailNotificationsSeverity + Microsoft Defender for Cloud 'Email notifications' roles: $MDfCEmailNotificationsRoles + Microsoft Defender for Cloud 'Email notifications' emails: $MDfCEmailNotificationsEmails + +"@) + + #region ScopeInsightsDefenderPlans + if ($arrayDefenderPlansSubscription) { + + $defenderPlanSubscriptionDeprecatedContainerRegistry = $false + $defenderPlanSubscriptionDeprecatedKubernetesService = $false + + $containerRegistryStandardCount = ($arrayDefenderPlansSubscription.Group.where( { $_.defenderPlan -eq 'ContainerRegistry' -and $_.defenderPlanTier -eq 'Standard' } )).Count + $kubernetesServiceStandardCount = ($arrayDefenderPlansSubscription.Group.where( { $_.defenderPlan -eq 'KubernetesService' -and $_.defenderPlanTier -eq 'Standard' } )).Count + if ($containerRegistryStandardCount -gt 0) { + $defenderPlanSubscriptionDeprecatedContainerRegistry = $true + } + if ($kubernetesServiceStandardCount -gt 0) { + $defenderPlanSubscriptionDeprecatedKubernetesService = $true + } + + $defenderCapabilitiesSubscription = ($arrayDefenderPlansSubscription.group.defenderPlan | Sort-Object -Unique) + $tfCount = 1 + $htmlTableId = "ScopeInsights_DefenderPlans_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+"@) + + if ($defenderPlanSubscriptionDeprecatedContainerRegistry) { + [void]$htmlScopeInsights.AppendLine(@' +    Using deprecated plan 'Container registries' docs
+'@) + } + if ($defenderPlanSubscriptionDeprecatedKubernetesService) { + [void]$htmlScopeInsights.AppendLine(@' +    Using deprecated plan 'Kubernetes' docs
+'@) + } + + [void]$htmlScopeInsights.AppendLine(@" +   Download CSV semicolon | comma + + + + + + + + + +"@) + + foreach ($plan in $arrayDefenderPlansSubscription.Group | Sort-Object -Property defenderPlan) { + if (($plan.defenderPlan -eq 'ContainerRegistry' -and $plan.defenderPlanTier -eq 'Standard') -or ($plan.defenderPlan -eq 'KubernetesService' -and $plan.defenderPlanTier -eq 'Standard')) { + $thisDefenderPlan = " $($plan.defenderPlan)" + } + else { + $thisDefenderPlan = $plan.defenderPlan + } + [void]$htmlScopeInsights.AppendLine(@" + + + + +"@) + } + [void]$htmlScopeInsights.AppendLine(@" + + +
PlanTier
$($thisDefenderPlan)$($plan.defenderPlanTier)
+ +
+"@) + } + else { + $subscriptionSkippedMDfC = $arrayDefenderPlansSubscriptionsSkipped.where( { $_.subscriptionId -eq $subscriptionId } ) + if ($subscriptionSkippedMDfC.Count -gt 0) { + if ($subscriptionSkippedMDfC.reason -eq 'SubScriptionNotRegistered') { + [void]$htmlScopeInsights.AppendLine(@" + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) (ResourceProvider: Microsoft.Security) docs +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + Microsoft Defender for Cloud plans - Subscription skipped ($($subscriptionSkippedMDfC.reason)) +"@) + } + + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Microsoft Defender for Cloud plans docs +'@) + } + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsDefenderPlans + + #region ScopeInsightsDiganosticsSubscription + if (($htDiagnosticSettingsMgSub).sub.($subscriptionId)) { + $diagnosticsSubCount = (($htDiagnosticSettingsMgSub).sub.($subscriptionId).Values.Count) + $tfCount = $diagnosticsSubCount + $htmlTableId = "ScopeInsights_DiagnosticsSub_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + +"@) + foreach ($logCategory in $diagnosticSettingsSubCategories) { + [void]$htmlScopeInsights.AppendLine(@" + +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + + +'@) + $htmlScopeInsightsDiagnosticsSub = $null + $htmlScopeInsightsDiagnosticsSub = foreach ($entry in ($htDiagnosticSettingsMgSub).sub.($subscriptionId).keys | Sort-Object) { + foreach ($diagset in ($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.keys | Sort-Object) { + @" + + + + +"@ + foreach ($logCategory in $diagnosticSettingsSubCategories) { + if (($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticCategoriesHt.($logCategory)) { + @" + +"@ + } + else { + @' + +'@ + } + } + @' + +'@ + } + } + + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsSub) + [void]$htmlScopeInsights.AppendLine(@" + +
Diagnostic settingTargetTarget Id$logCategory
$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticSettingName)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticTargetType)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticTargetId)$(($htDiagnosticSettingsMgSub).sub.($subscriptionId).$entry.$diagset.DiagnosticCategoriesHt.($logCategory))n/a
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Subscription Diagnostic settings docs +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsDiganosticsSubscription + + #Tags + #region ScopeInsightsTags + $tagsSubscriptionCount = ($htSubscriptionTags.$subscriptionId.Keys).count + if ($tagsSubscriptionCount -gt 0) { + $tfCount = $tagsSubscriptionCount + $htmlTableId = "ScopeInsights_Tags_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + +"@) + $htmlScopeInsightsTags = $null + $htmlScopeInsightsTags = foreach ($tag in (($htSubscriptionTags).($subscriptionId)).keys | Sort-Object) { + @" + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsTags) + [void]$htmlScopeInsights.AppendLine(@" + +
Tag NameTag Value
$tag$($htSubscriptionTags.$subscriptionId[$tag])
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $tagsSubscriptionCount Subscription Tags +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsTags + + #TagNameUsage + #region ScopeInsightsTagNameUsage + $arrayTagListSubscription = [System.Collections.ArrayList]@() + foreach ($tagScope in $htSubscriptionTagList.($subscriptionId).keys) { + foreach ($tagScopeTagName in $htSubscriptionTagList.($subscriptionId).$tagScope.keys) { + $null = $arrayTagListSubscription.Add([PSCustomObject]@{ + Scope = $tagScope + TagName = ($tagScopeTagName) + TagCount = $htSubscriptionTagList.($subscriptionId).($tagScope).($tagScopeTagName) + }) + } + } + $tagsUsageCount = ($arrayTagListSubscription).Count + + if ($tagsUsageCount -gt 0) { + $tagNamesUniqueCount = ($arrayTagListSubscription | Sort-Object -Property TagName -Unique).Count + $tagNamesUsedInScopes = ($arrayTagListSubscription | Sort-Object -Property Scope -Unique).scope -join "$($CsvDelimiterOpposite) " + $tfCount = $tagsUsageCount + $htmlTableId = "ScopeInsights_TagNameUsage_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Resource naming and tagging decision guide docs
+   Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlScopeInsightsTagsUsage = $null + $htmlScopeInsightsTagsUsage = foreach ($tagEntry in $arrayTagListSubscription | Sort-Object Scope, TagName -CaseSensitive) { + @" + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsTagsUsage) + [void]$htmlScopeInsights.AppendLine(@" + +
ScopeTagNameCount
$($tagEntry.Scope)$($tagEntry.TagName)$($tagEntry.TagCount)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + Tag Name Usage ($tagsUsageCount Tags) docs +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsTagNameUsage + + #Consumption + #$startScopeInsightsConsumptionSub = Get-Date + #region ScopeInsightsConsumptionSub + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + + if ($htAzureConsumptionSubscriptions.($subscriptionId).ConsumptionData) { + $consumptionData = $htAzureConsumptionSubscriptions.($subscriptionId).ConsumptionData + + $arrayTotalCostSummarySub = @() + $arrayConsumptionData = [System.Collections.ArrayList]@() + + $totalCost = 0 + + $currency = $htAzureConsumptionSubscriptions.($subscriptionId).Currency + $consumedServiceCount = ($consumptionData.ResourceType | Sort-Object -Unique | Measure-Object).Count + $resourceCount = ($consumptionData.ResourceId | Sort-Object -Unique | Measure-Object).Count + $subConsumptionDataGrouped = $consumptionData | Group-Object -Property ResourceType, ChargeType, MeterCategory + + foreach ($consumptionline in $subConsumptionDataGrouped) { + + $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum + if ([math]::Round($costConsumptionLine, 2) -eq 0) { + $cost = $costConsumptionLine.ToString('0.0000') + } + else { + $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') + } + + $null = $arrayConsumptionData.Add([PSCustomObject]@{ + ResourceType = ($consumptionline.name).split(', ')[0] + ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] + ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] + ConsumedServiceInstanceCount = $consumptionline.Count + ConsumedServiceCost = $cost #[decimal]$cost + ConsumedServiceCurrency = $currency + }) + + $totalCost = $htAzureConsumptionSubscriptions.($subscriptionId).TotalCost + + } + if ([math]::Round($totalCost, 2) -eq 0) { + $totalCost = $totalCost + } + else { + $totalCost = [math]::Round($totalCost, 2).ToString('0.00') + } + $arrayTotalCostSummarySub += "$($totalCost) $($currency) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes)" + + $tfCount = ($arrayConsumptionData | Measure-Object).Count + $htmlTableId = "ScopeInsights_Consumption_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsConsumptionSub = $null + $htmlScopeInsightsConsumptionSub = foreach ($consumptionLine in $arrayConsumptionData) { + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsConsumptionSub) + [void]$htmlScopeInsights.AppendLine(@" + +
ChargeTypeResourceTypeCategoryResourceCountCost ($($AzureConsumptionPeriod)d)Currency
$($consumptionLine.ConsumedServiceChargeType)$($consumptionLine.ResourceType)$($consumptionLine.ConsumedServiceCategory)$($consumptionLine.ConsumedServiceInstanceCount)$($consumptionLine.ConsumedServiceCost)$($currency)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Consumption data available +'@) + } + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Consumption data available as switch parameter -DoAzureConsumption was not applied +'@) + } + + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsConsumptionSub + #$endScopeInsightsConsumptionSub = Get-Date + #Write-Host " **ScopeInsightsConsumptionSub data duration: $((NEW-TIMESPAN -Start $startScopeInsightsConsumptionSub -End $endScopeInsightsConsumptionSub).TotalSeconds) seconds" + + #ResourceGroups + #region ScopeInsightsResourceGroups + if ($subscriptionResourceGroupsCount -gt 0) { + [void]$htmlScopeInsights.AppendLine(@" + $subscriptionResourceGroupsCount Resource Groups | Limit: ($subscriptionResourceGroupsCount/$LimitResourceGroups) +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $subscriptionResourceGroupsCount Resource Groups +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsResourceGroups + + #ResourceProvider + #region ScopeInsightsResourceProvidersDetailed + if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { + if ($azAPICallConf['htParameters'].NoResourceProvidersDetailed -eq $false) { + if (($htResourceProvidersAll).($subscriptionId)) { + $tfCount = ($htResourceProvidersAll).($subscriptionId).Providers.Count + $htmlTableId = "ScopeInsights_ResourceProvider_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + +"@) + $htmlScopeInsightsResourceProvidersDetailed = $null + $htmlScopeInsightsResourceProvidersDetailed = foreach ($provider in ($htResourceProvidersAll).($subscriptionId).Providers) { + @" + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResourceProvidersDetailed) + [void]$htmlScopeInsights.AppendLine(@" + +
ProviderState
$($provider.namespace)$($provider.registrationState)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $(($htResourceProvidersAll.Keys).count) Resource Providers +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + } + #endregion ScopeInsightsResourceProvidersDetailed + + #region ScopeInsightsSubscriptionFeatures + if ($subscriptionFeatures) { + $subscriptionFeaturesCount = $subscriptionFeatures.Group.Count + + $tfCount = $subscriptionFeaturesCount + $htmlTableId = "ScopeInsights_SubscriptionFeatures_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + + [void]$htmlScopeInsights.AppendLine(@" + +
+   Set up preview features in Azure subscription docs + + + + + + + +"@) + + foreach ($feature in $subscriptionFeatures.Group | Sort-Object -Property feature) { + [void]$htmlScopeInsights.AppendLine(@" + +"@) + } + + + [void]$htmlScopeInsights.AppendLine(@" + +
Feature
$($feature.feature)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + 0 enabled Subscription Features docs +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsSubscriptionFeatures + + #ResourceLocks + #region ScopeInsightsResourceLocks + if ($htResourceLocks.($subscriptionId)) { + $tfCount = 6 + $htmlTableId = "ScopeInsights_ResourceLocks_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + + $subscriptionLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).SubscriptionLocksCannotDeleteCount + $subscriptionLocksReadOnlyCount = $htResourceLocks.($subscriptionId).SubscriptionLocksReadOnlyCount + $resourceGroupsLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).ResourceGroupsLocksCannotDeleteCount + $resourceGroupsLocksReadOnlyCount = $htResourceLocks.($subscriptionId).ResourceGroupsLocksReadOnlyCount + $resourcesLocksCannotDeleteCount = $htResourceLocks.($subscriptionId).ResourcesLocksCannotDeleteCount + $resourcesLocksReadOnlyCount = $htResourceLocks.($subscriptionId).ResourcesLocksReadOnlyCount + + [void]$htmlScopeInsights.AppendLine(@" + +
+   Considerations before applying locks docs + + + + + + + + + + + + + + + + +
Lock scopeLock typepresence
SubscriptionCannotDelete$($subscriptionLocksCannotDeleteCount)
SubscriptionReadOnly$($subscriptionLocksReadOnlyCount)
ResourceGroupCannotDelete$($resourceGroupsLocksCannotDeleteCount)
ResourceGroupReadOnly$($resourceGroupsLocksReadOnlyCount)
ResourceCannotDelete$($resourcesLocksCannotDeleteCount)
ResourceReadOnly$($resourcesLocksReadOnlyCount)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + 0 Resource Locks docs +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsResourceLocks + + } + + #MgChildInfo + #region ScopeInsightsManagementGroups + if ($mgOrSub -eq 'mg') { + + [void]$htmlScopeInsights.AppendLine(@" +$(($mgAllChildMgs).count -1) ManagementGroups below this scope +$(($mgAllChildSubscriptions).count) Subscriptions below this scope + Microsoft Defender for Cloud Secure Score: $managementGroupASCPoints Video , Blog , docs + +"@) + + #region ScopeInsightsDiagnosticsMg + if (($htDiagnosticSettingsMgSub).mg.($mgChild)) { + $diagnosticsMgCount = (($htDiagnosticSettingsMgSub).mg.($mgChild).Values.Count) + $tfCount = $diagnosticsMgCount + $htmlTableId = "ScopeInsights_DiagnosticsMg_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + +"@) + foreach ($logCategory in $diagnosticSettingsMgCategories) { + [void]$htmlScopeInsights.AppendLine(@" + +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + + +'@) + $htmlScopeInsightsDiagnosticsMg = $null + $htmlScopeInsightsDiagnosticsMg = foreach ($entry in ($htDiagnosticSettingsMgSub).mg.($mgChild).keys | Sort-Object) { + foreach ($diagset in ($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.keys | Sort-Object) { + @" + + + + +"@ + foreach ($logCategory in $diagnosticSettingsMgCategories) { + if (($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticCategoriesHt.($logCategory)) { + @" + +"@ + } + else { + @' + +'@ + } + } + @' + +'@ + } + } + + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsMg) + [void]$htmlScopeInsights.AppendLine(@" + +
Diagnostic settingTargetTarget Id$logCategory
$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticSettingName)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticTargetType)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticTargetId)$(($htDiagnosticSettingsMgSub).mg.($mgChild).$entry.$diagset.DiagnosticCategoriesHt.($logCategory))n/a
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Management Group Diagnostic settings docs +'@) + } + #endregion ScopeInsightsDiagnosticsMg + + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + + #$startScopeInsightsConsumptionMg = Get-Date + #region ScopeInsightsConsumptionMg + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($allConsumptionDataCount -gt 0) { + + $consumptionData = $htManagementGroupsCost.($mgchild).consumptionDataSubscriptions + if (($consumptionData | Measure-Object).Count -gt 0) { + $arrayTotalCostSummaryMg = @() + $arrayConsumptionData = [System.Collections.ArrayList]@() + $consumptionDataGroupedByCurrency = $consumptionData | Group-Object -Property Currency + foreach ($currency in $consumptionDataGroupedByCurrency) { + $totalCost = 0 + $tenantSummaryConsumptionDataGrouped = $currency.group | Group-Object -Property ResourceType, ChargeType, MeterCategory + $subsCount = ($tenantSummaryConsumptionDataGrouped.group.subscriptionId | Sort-Object -Unique).Count + $consumedServiceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceType | Sort-Object -Unique).Count + $resourceCount = ($tenantSummaryConsumptionDataGrouped.group.ResourceId | Sort-Object -Unique).Count + foreach ($consumptionline in $tenantSummaryConsumptionDataGrouped) { + + $costConsumptionLine = ($consumptionline.group.PreTaxCost | Measure-Object -Sum).Sum + if ([math]::Round($costConsumptionLine, 2) -eq 0) { + $cost = $costConsumptionLine.ToString('0.0000') + } + else { + $cost = [math]::Round($costConsumptionLine, 2).ToString('0.00') + } + + $null = $arrayConsumptionData.Add([PSCustomObject]@{ + ResourceType = ($consumptionline.name).split(', ')[0] + ConsumedServiceChargeType = ($consumptionline.name).split(', ')[1] + ConsumedServiceCategory = ($consumptionline.name).split(', ')[2] + ConsumedServiceInstanceCount = $consumptionline.Count + ConsumedServiceCost = $cost #[decimal]$cost + ConsumedServiceSubscriptions = ($consumptionline.group.SubscriptionId | Sort-Object -Unique).Count + ConsumedServiceCurrency = $currency.Name + }) + + $totalCost = $totalCost + $costConsumptionLine + } + if ([math]::Round($totalCost, 2) -eq 0) { + $totalCost = $totalCost.ToString('0.0000') + } + else { + $totalCost = [math]::Round($totalCost, 2).ToString('0.00') + } + $arrayTotalCostSummaryMg += "$($totalCost) $($currency.Name) generated by $($resourceCount) Resources ($($consumedServiceCount) ResourceTypes) in $($subsCount) Subscriptions" + } + + $tfCount = ($arrayConsumptionData).Count + $htmlTableId = "ScopeInsights_Consumption_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV +semicolon | +comma + + + + + + + + + + + + + +"@) + $htmlScopeInsightsConsumptionMg = $null + $htmlScopeInsightsConsumptionMg = foreach ($consumptionLine in $arrayConsumptionData) { + @" + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsConsumptionMg) + [void]$htmlScopeInsights.AppendLine(@" + +
ChargeTypeResourceTypeCategoryResourceCountCost ($($AzureConsumptionPeriod)d)CurrencySubscriptions
$($consumptionLine.ConsumedServiceChargeType)$($consumptionLine.ResourceType)$($consumptionLine.ConsumedServiceCategory)$($consumptionLine.ConsumedServiceInstanceCount)$($consumptionLine.ConsumedServiceCost)$($consumptionLine.ConsumedServiceCurrency)$($consumptionLine.ConsumedServiceSubscriptions)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Consumption data available for Subscriptions under this ManagementGroup +'@) + } + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Consumption data available +'@) + } + + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Consumption data available as switch parameter -DoAzureConsumption was not applied +'@) + } + #endregion ScopeInsightsConsumptionMg + #$endScopeInsightsConsumptionMg = Get-Date + #Write-Host " ++ScopeInsightsConsumptionMg duration: ($((NEW-TIMESPAN -Start $startScopeInsightsConsumptionMg -End $endScopeInsightsConsumptionMg).TotalSeconds) seconds)" + + + } + #endregion ScopeInsightsManagementGroups + + #ScopeInsightsResources + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #resources + #region ScopeInsightsResources + if ($mgOrSub -eq 'mg') { + if ($resourcesAllChildSubscriptionLocationCount -gt 0) { + $tfCount = ($resourcesAllChildSubscriptionsArray).count + $htmlTableId = "ScopeInsights_Resources_$($mgChild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlScopeInsightsResources = $null + $htmlScopeInsightsResources = foreach ($resourceAllChildSubscriptionResourceTypePerLocation in $resourcesAllChildSubscriptionsArray | Sort-Object @{Expression = { $_.ResourceType } }, @{Expression = { $_.location } }) { + @" + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResources) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeLocationCount
$($resourceAllChildSubscriptionResourceTypePerLocation.ResourceType)$($resourceAllChildSubscriptionResourceTypePerLocation.location)$($resourceAllChildSubscriptionResourceTypePerLocation.ResourceCount)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $resourcesAllChildSubscriptionResourceTypeCount ResourceTypes (all Subscriptions below this scope) +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + + if ($mgOrSub -eq 'sub') { + if ($resourcesSubscriptionResourceTypeCount -gt 0) { + $tfCount = ($resourcesSubscription).Count + $htmlTableId = "ScopeInsights_Resources_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlScopeInsightsResources = $null + $htmlScopeInsightsResources = foreach ($resourceSubscriptionResourceTypePerLocation in $resourcesSubscription | Sort-Object @{Expression = { $_.type } }, @{Expression = { $_.location } }, @{Expression = { $_.count_ } }) { + @" + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsResources) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeLocationCount
$($resourceSubscriptionResourceTypePerLocation.type)$($resourceSubscriptionResourceTypePerLocation.location)$($resourceSubscriptionResourceTypePerLocation.count_)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $resourcesSubscriptionResourceTypeCount ResourceTypes +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsResources + } + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region ScopeInsightsCAFResourceNamingALL + if ($mgOrSub -eq 'sub') { + $resourcesIdsAllCAFNamingRelevantThisSubscription = $resourcesIdsAllCAFNamingRelevantGroupedBySubscription.where({ $_.Name -eq $subscriptionId }) + if ($resourcesIdsAllCAFNamingRelevantThisSubscription) { + $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType = $resourcesIdsAllCAFNamingRelevantThisSubscription.Group | Group-Object -Property type + $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByTypeCount = ($resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType | Measure-Object).Count + + $tfCount = $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByTypeCount + $htmlTableId = "ScopeInsights_CAFResourceNamingALL_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   CAF - Recommended abbreviations for Azure resource types docs
+   Resource details can be found in the CSV output *_ResourcesAll.csv
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsCAFResourceNamingALL = $null + $htmlScopeInsightsCAFResourceNamingALL = foreach ($entry in $resourcesIdsAllCAFNamingRelevantThisSubscriptionGroupedByType) { + + $resourceTypeGroupedByCAFResourceNamingResult = $entry.Group | Group-Object -Property cafResourceNamingResult, cafResourceNaming + if ($entry.Group.cafResourceNaming.Count -gt 1) { + $namingConvention = ($entry.Group.cafResourceNaming)[0] + $namingConventionFriendlyName = ($entry.Group.cafResourceNamingFriendlyName)[0] + } + else { + $namingConvention = $entry.Group.cafResourceNaming + $namingConventionFriendlyName = $entry.Group.cafResourceNamingFriendlyName + } + + $passed = 0 + $failed = 0 + foreach ($result in $resourceTypeGroupedByCAFResourceNamingResult) { + $resultNameSplitted = $result.Name -split ', ' + if ($resultNameSplitted[0] -eq 'passed') { + $passed = $result.Count + } + + if ($resultNameSplitted[0] -eq 'failed') { + $failed = $result.Count + } + } + + if ($passed -gt 0) { + $percentage = [math]::Round(($passed / ($passed + $failed) * 100), 2) + } + else { + $percentage = 0 + } + + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsCAFResourceNamingALL) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeRecommendationResourceFriendlyNamepassedfailedpassed percentage
$($entry.Name)$($namingConvention)$($namingConventionFriendlyName)$($passed)$($failed)$($percentage)%
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No CAF Naming Recommendation Compliance data available +'@) + } + + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsCAFResourceNamingALL + } + + #region ScopeInsightsOrphanedResources + if ($mgOrSub -eq 'sub') { + if ($arrayOrphanedResourcesGroupedBySubscription) { + $orphanedResourcesThisSubscription = $arrayOrphanedResourcesGroupedBySubscription.where({ $_.Name -eq $subscriptionId }) + if ($orphanedResourcesThisSubscription) { + $orphanedResourcesThisSubscriptionCount = $orphanedResourcesThisSubscription.Group.count + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + $orphanedIncludingCost = $true + $hintTableTH = " ($($AzureConsumptionPeriod) days)" + + $orphanedResourcesThisSubscriptionGroupedByType = $orphanedResourcesThisSubscription.Group | Group-Object -Property type, currency + $orphanedResourcesThisSubscriptionGroupedByTypeCount = ($orphanedResourcesThisSubscriptionGroupedByType | Measure-Object).Count + } + else { + $orphanedIncludingCost = $false + $hintTableTH = '' + + $orphanedResourcesThisSubscriptionGroupedByType = $orphanedResourcesThisSubscription.Group | Group-Object -Property type + $orphanedResourcesThisSubscriptionGroupedByTypeCount = ($orphanedResourcesThisSubscriptionGroupedByType | Measure-Object).Count + } + + $tfCount = $orphanedResourcesThisSubscriptionGroupedByTypeCount + $htmlTableId = "ScopeInsights_OrphanedResources_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   'Azure Orphan Resources' ARG queries and workbooks GitHub
+   Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
+   Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlScopeInsightsOrphanedResources = $null + $htmlScopeInsightsOrphanedResources = foreach ($resourceType in $orphanedResourcesThisSubscriptionGroupedByType | Sort-Object -Property Name) { + + if ($orphanedIncludingCost) { + if (($resourceType.Group[0].Intent) -like 'cost savings*') { + $orphCost = ($resourceType.Group.Cost | Measure-Object -Sum).Sum + if ($orphCost -eq 0) { + $orphCost = '' + } + $orphCurrency = $resourceType.Group[0].Currency + } + else { + $orphCost = '' + $orphCurrency = '' + } + } + else { + if (($resourceType.Group.Intent | Get-Unique) -like 'cost savings*') { + $orphCost = "use parameter -DoAzureConsumption to show potential savings" + $orphCurrency = '' + } + else { + $orphCost = '' + $orphCurrency = '' + } + } + + @" + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsOrphanedResources) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeResource countIntentCost$($hintTableTH)Currency
$(($resourceType.Name -split ',')[0])$($resourceType.Group.Count)$($resourceType.Group[0].Intent)$($orphCost)$($orphCurrency)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No cost optimization & cleanup +'@) + } + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No cost optimization & cleanup +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsOrphanedResources + + #ScopeInsightsDiagnosticsCapable + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #resourcesDiagnosticsCapable + #region ScopeInsightsDiagnosticsCapable + if ($mgOrSub -eq 'mg') { + $resourceTypesUnique = ($resourcesAllChildSubscriptions | Select-Object type -Unique).type + $resourceTypesSummarizedArray = [System.Collections.ArrayList]@() + foreach ($resourceTypeUnique in $resourceTypesUnique) { + $resourcesTypeCountTotal = 0 + ($resourcesAllChildSubscriptions.where( { $_.type -eq $resourceTypeUnique } )).count_ | ForEach-Object { $resourcesTypeCountTotal += $_ } + $dataFromResourceTypesDiagnosticsArray = $resourceTypesDiagnosticsArray.where( { $_.ResourceType -eq $resourceTypeUnique } ) + if ($dataFromResourceTypesDiagnosticsArray.Metrics -eq $true -or $dataFromResourceTypesDiagnosticsArray.Logs -eq $true) { + $resourceDiagnosticscapable = $true + } + else { + $resourceDiagnosticscapable = $false + } + $null = $resourceTypesSummarizedArray.Add([PSCustomObject]@{ + ResourceType = $resourceTypeUnique + ResourceCount = $resourcesTypeCountTotal + DiagnosticsCapable = $resourceDiagnosticscapable + Metrics = $dataFromResourceTypesDiagnosticsArray.Metrics + Logs = $dataFromResourceTypesDiagnosticsArray.Logs + LogCategories = ($dataFromResourceTypesDiagnosticsArray.LogCategories -join "$CsvDelimiterOpposite ") + }) + } + $subscriptionResourceTypesDiagnosticsCapableMetricsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true } )).count + $subscriptionResourceTypesDiagnosticsCapableLogsCount = ($resourceTypesSummarizedArray.where( { $_.Logs -eq $true } )).count + $subscriptionResourceTypesDiagnosticsCapableMetricsLogsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true -or $_.Logs -eq $true } )).count + + if ($resourcesAllChildSubscriptionResourceTypeCount -gt 0) { + $tfCount = $resourcesAllChildSubscriptionResourceTypeCount + $htmlTableId = "ScopeInsights_resourcesDiagnosticsCapable_$($mgchild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsDiagnosticsCapable = $null + $htmlScopeInsightsDiagnosticsCapable = foreach ($resourceSubscriptionResourceType in $resourceTypesSummarizedArray | Sort-Object @{Expression = { $_.ResourceType } }) { + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsCapable) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeResource CountDiagnostics capableMetricsLogsLogCategories
$($resourceSubscriptionResourceType.ResourceType)$($resourceSubscriptionResourceType.ResourceCount)$($resourceSubscriptionResourceType.DiagnosticsCapable)$($resourceSubscriptionResourceType.Metrics)$($resourceSubscriptionResourceType.Logs)$($resourceSubscriptionResourceType.LogCategories)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $resourcesAllChildSubscriptionResourceTypeCount ResourceTypes (1st party) Diagnostics capable (all Subscriptions below this scope) +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + + if ($mgOrSub -eq 'sub') { + $resourceTypesUnique = ($resourcesSubscription | Select-Object type -Unique).type + $resourceTypesSummarizedArray = [System.Collections.ArrayList]@() + foreach ($resourceTypeUnique in $resourceTypesUnique) { + $resourcesTypeCountTotal = 0 + ($resourcesSubscription.where( { $_.type -eq $resourceTypeUnique } )).count_ | ForEach-Object { $resourcesTypeCountTotal += $_ } + $dataFromResourceTypesDiagnosticsArray = $resourceTypesDiagnosticsArray.where( { $_.ResourceType -eq $resourceTypeUnique } ) + if ($dataFromResourceTypesDiagnosticsArray.Metrics -eq $true -or $dataFromResourceTypesDiagnosticsArray.Logs -eq $true) { + $resourceDiagnosticscapable = $true + } + else { + $resourceDiagnosticscapable = $false + } + $null = $resourceTypesSummarizedArray.Add([PSCustomObject]@{ + ResourceType = $resourceTypeUnique + ResourceCount = $resourcesTypeCountTotal + DiagnosticsCapable = $resourceDiagnosticscapable + Metrics = $dataFromResourceTypesDiagnosticsArray.Metrics + Logs = $dataFromResourceTypesDiagnosticsArray.Logs + LogCategories = ($dataFromResourceTypesDiagnosticsArray.LogCategories -join "$CsvDelimiterOpposite ") + }) + } + + $subscriptionResourceTypesDiagnosticsCapableMetricsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true } )).count + $subscriptionResourceTypesDiagnosticsCapableLogsCount = ($resourceTypesSummarizedArray.where( { $_.Logs -eq $true } )).count + $subscriptionResourceTypesDiagnosticsCapableMetricsLogsCount = ($resourceTypesSummarizedArray.where( { $_.Metrics -eq $true -or $_.Logs -eq $true } )).count + + if ($resourcesSubscriptionResourceTypeCount -gt 0) { + $tfCount = $resourcesSubscriptionResourceTypeCount + $htmlTableId = "ScopeInsights_resourcesDiagnosticsCapable_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsDiagnosticsCapable = $null + $htmlScopeInsightsDiagnosticsCapable = foreach ($resourceSubscriptionResourceType in $resourceTypesSummarizedArray | Sort-Object @{Expression = { $_.ResourceType } }) { + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsDiagnosticsCapable) + [void]$htmlScopeInsights.AppendLine(@" + +
ResourceTypeResource CountDiagnostics capableMetricsLogsLogCategories
$($resourceSubscriptionResourceType.ResourceType)$($resourceSubscriptionResourceType.ResourceCount)$($resourceSubscriptionResourceType.DiagnosticsCapable)$($resourceSubscriptionResourceType.Metrics)$($resourceSubscriptionResourceType.Logs)$($resourceSubscriptionResourceType.LogCategories)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $resourcesSubscriptionResourceTypeCount ResourceTypes (1st party) Diagnostics capable +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsDiagnosticsCapable + } + + #ScopeInsightsUserAssignedIdentities4Resources + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + if ($mgOrSub -eq 'sub') { + #region ScopeInsightsUserAssignedIdentities4Resources + if ($arrayUserAssignedIdentities4ResourcesSubscriptionCount -gt 0) { + $tfCount = $arrayUserAssignedIdentities4ResourcesSubscriptionCount + $htmlTableId = "ScopeInsights_UserAssignedIdentities4Resources_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Managed identity 'user-assigned' vs 'system-assigned' docs
+   Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlScopeInsightsUserAssignedIdentities4Resource = $null + $htmlScopeInsightsUserAssignedIdentities4Resource = foreach ($miResEntry in $arrayUserAssignedIdentities4ResourcesSubscription | Sort-Object -Property miResourceId, resourceId) { + @" + + + + + + + + + + + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsUserAssignedIdentities4Resource) + [void]$htmlScopeInsights.AppendLine(@" + +
MI NameMI MgPathMI Subscription NameMI Subscription IdMI ResourceGroupMI ResourceIdMI AAD SP objectIdMI AAD SP applicationIdMI count Res assignmentsMI used cross subscriptionRes NameRes TypeRes MgPathRes Subscription NameRes Subscription IdRes ResourceGroupRes IdRes count assigned MIs
$($miResEntry.miResourceName)$($miResEntry.miMgPath)$($miResEntry.miSubscriptionName)$($miResEntry.miSubscriptionId)$($miResEntry.miResourceGroupName)$($miResEntry.miResourceId)$($miResEntry.miPrincipalId)$($miResEntry.miClientId)$($htUserAssignedIdentitiesAssignedResources.($miResEntry.miPrincipalId).ResourcesCount)$($miResEntry.miCrossSubscription)$($miResEntry.resourceName)$($miResEntry.resourceType)$($miResEntry.resourceMgPath)$($miResEntry.resourceSubscriptionName)$($miResEntry.resourceSubscriptionId)$($miResEntry.resourceResourceGroupName)$($miResEntry.resourceId)$($htResourcesAssignedUserAssignedIdentities.(($miResEntry.resourceId).tolower()).UserAssignedIdentitiesCount)
+ +
+"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No UserAssigned Managed Identities assigned to Resources / vice versa - at all +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsUserAssignedIdentities4Resources + } + } + + #ScopeInsightsPSRule + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { + #region ScopeInsightsPSRule + + if ($mgOrSub -eq 'mg') { + + $allPSRuleResultsUnderThisMg = [system.collections.ArrayList]@() + foreach ($mg in $grpPSRuleManagementGroups) { + if ($htManagementGroupsMgPath.($mg.name -replace '.*/').path -contains $mgchild) { + $allPSRuleResultsUnderThisMg.AddRange($mg.Group) + } + } + + $grpThisManagementGroup = $allPSRuleResultsUnderThisMg | Group-Object -Property resourceType, pillar, category, severity, rule, result + + if ($grpThisManagementGroup) { + $grpThisManagementGroupCount = $grpThisManagementGroup.Count + $tfCount = $grpThisManagementGroupCount + $htmlTableId = "ScopeInsights_PSRule_$($mgchild -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Learn about PSRule for Azure
+   Download CSV semicolon | comma + + + + + + + + + + + + + + + + +"@) + $htmlScopeInsightsPSRuleMG = $null + $htmlScopeInsightsPSRuleMG = foreach ($result in $grpThisManagementGroup) { + $resultNameSplit = $result.Name.split(', ') + @" + + + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPSRuleMG) + [void]$htmlScopeInsights.AppendLine(@" + +
Resource TypeResource CountSubscription CountPillarCategorySeverityRuleRecommendationlnkState
$($resultNameSplit[0])$($result.Group.Count)$(($result.Group.subscriptionId | Sort-Object -Unique).Count)$($resultNameSplit[1])$($resultNameSplit[2])$($resultNameSplit[3])$(($result.Group[0].rule))$(($result.Group[0].recommendation))$($resultNameSplit[5])
+ +
+"@) + + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No PSRule for Azure results +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + + if ($mgOrSub -eq 'sub') { + $grpThisSubscription = $grpPSRuleSubscriptions.where({ $_.Name -eq $subscriptionId }) + $grpThisSubscriptionGrouped = $grpThisSubscription.Group | Group-Object -Property resourceType, pillar, category, severity, rule, result + + if ($grpThisSubscriptionGrouped) { + $grpThisSubscriptionGroupedCount = $grpThisSubscriptionGrouped.Count + $tfCount = $grpThisSubscriptionGroupedCount + $htmlTableId = "ScopeInsights_PSRule_$($subscriptionId -replace '-','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Learn about PSRule for Azure
+   Download CSV semicolon | comma + + + + + + + + + + + + + + + +"@) + $htmlScopeInsightsPSRuleSub = $null + $htmlScopeInsightsPSRuleSub = foreach ($result in $grpThisSubscriptionGrouped) { + $resultNameSplit = $result.Name.split(', ') + @" + + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPSRuleSub) + [void]$htmlScopeInsights.AppendLine(@" + +
Resource TypeResource CountPillarCategorySeverityRuleRecommendationlnkState
$($resultNameSplit[0])$($result.Group.Count)$($resultNameSplit[1])$($resultNameSplit[2])$($resultNameSplit[3])$(($result.Group[0].rule))$(($result.Group[0].recommendation))$($resultNameSplit[5])
+ +
+"@) + + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No PSRule results +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsPSRule + } + else { + [void]$htmlScopeInsights.AppendLine(@' + PSRule for Azure - integration paused - PSRule for Azure +'@) + } + } + + #PolicyAssignments + #region ScopeInsightsPolicyAssignments + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + + $policiesAssigned = [System.Collections.ArrayList]@() + $policiesCount = 0 + $policiesCountBuiltin = 0 + $policiesCountCustom = 0 + $policiesAssignedAtScope = 0 + $policiesInherited = 0 + foreach ($policyAssignment in $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicy) { + if ([String]::IsNullOrEmpty($policyAssignment.subscriptionId)) { + $null = $policiesAssigned.Add($policyAssignment) + $policiesCount++ + if ($policyAssignment.PolicyType -eq 'BuiltIn') { + $policiesCountBuiltin++ + } + if ($policyAssignment.PolicyType -eq 'Custom') { + $policiesCountCustom++ + } + if ($policyAssignment.Inheritance -like 'this*') { + $policiesAssignedAtScope++ + } + if ($policyAssignment.Inheritance -notlike 'this*') { + $policiesInherited++ + } + } + } + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + + $policiesAssigned = [System.Collections.ArrayList]@() + $policiesCount = 0 + $policiesCountBuiltin = 0 + $policiesCountCustom = 0 + $policiesAssignedAtScope = 0 + $policiesInherited = 0 + foreach ($policyAssignment in $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicy) { + $null = $policiesAssigned.Add($policyAssignment) + $policiesCount++ + if ($policyAssignment.PolicyType -eq 'BuiltIn') { + $policiesCountBuiltin++ + } + if ($policyAssignment.PolicyType -eq 'Custom') { + $policiesCountCustom++ + } + if ($policyAssignment.Inheritance -like 'this*') { + $policiesAssignedAtScope++ + } + if ($policyAssignment.Inheritance -notlike 'this*') { + $policiesInherited++ + } + } + } + + if (($policiesAssigned).count -gt 0) { + $tfCount = ($policiesAssigned).count + $htmlTableId = "ScopeInsights_PolicyAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + $noteOrNot = '' + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma
+  *Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience + + + + + + + + + + + + + + + +"@) + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + + [void]$htmlScopeInsights.AppendLine(@' + + + + + +'@) + } + + [void]$htmlScopeInsights.AppendLine(@" + + + + + + + + + + + + +"@) + $htmlScopeInsightsPolicyAssignments = $null + $htmlScopeInsightsPolicyAssignments = foreach ($policyAssignment in $policiesAssigned | Sort-Object @{Expression = { $_.Level } }, @{Expression = { $_.MgName } }, @{Expression = { $_.MgId } }, @{Expression = { $_.SubscriptionName } }, @{Expression = { $_.SubscriptionId } }, @{Expression = { $_.PolicyAssignmentId } }) { + + if ($policyAssignment.PolicyType -eq 'Custom') { + $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') + } + else { + $policyName = $policyAssignment.PolicyName + } + @" + + + + + + + + + + + + + +"@ + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + @" + + + + + +"@ + } + + @" + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPolicyAssignments) + [void]$htmlScopeInsights.AppendLine(@" + +
InheritanceScopeExcludedExemption appliesPolicy DisplayNamePolicyIdTypeCategoryALZEffectParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotManaged IdentityAssignment DisplayNameAssignmentIdAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyAssignment.ExemptionScope)$($policyName)$($policyAssignment.PolicyId)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyIsALZ)$($policyAssignment.Effect)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages)$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentMI)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $(($policiesAssigned).count) Policy assignments +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsPolicyAssignments + + #PolicySetAssignments + #region ScopeInsightsPolicySetAssignments + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + + $policySetsAssigned = [System.Collections.ArrayList]@() + $policySetsCount = 0 + $policySetsCountBuiltin = 0 + $policySetsCountCustom = 0 + $policySetsAssignedAtScope = 0 + $policySetsInherited = 0 + foreach ($policySetAssignment in $arrayPolicyAssignmentsEnrichedForThisManagementGroupVariantPolicySet) { + if ([String]::IsNullOrEmpty($policySetAssignment.subscriptionId)) { + $null = $policySetsAssigned.Add($policySetAssignment) + $policySetsCount++ + if ($policySetAssignment.PolicyType -eq 'BuiltIn') { + $policySetsCountBuiltin++ + } + if ($policySetAssignment.PolicyType -eq 'Custom') { + $policySetsCountCustom++ + } + if ($policySetAssignment.Inheritance -like 'this*') { + $policySetsAssignedAtScope++ + } + if ($policySetAssignment.Inheritance -notlike 'this*') { + $policySetsInherited++ + } + } + } + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + + $policySetsAssigned = [System.Collections.ArrayList]@() + $policySetsCount = 0 + $policySetsCountBuiltin = 0 + $policySetsCountCustom = 0 + $policySetsAssignedAtScope = 0 + $policySetsInherited = 0 + foreach ($policySetAssignment in $arrayPolicyAssignmentsEnrichedForThisSubscriptionVariantPolicySet) { + $null = $policySetsAssigned.Add($policySetAssignment) + $policySetsCount++ + if ($policySetAssignment.PolicyType -eq 'BuiltIn') { + $policySetsCountBuiltin++ + } + if ($policySetAssignment.PolicyType -eq 'Custom') { + $policySetsCountCustom++ + } + if ($policySetAssignment.Inheritance -like 'this*') { + $policySetsAssignedAtScope++ + } + if ($policySetAssignment.Inheritance -notlike 'this*') { + $policySetsInherited++ + } + } + } + + if (($policySetsAssigned).count -gt 0) { + $tfCount = ($policiesAssigned).count + $htmlTableId = "ScopeInsights_PolicySetAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + $noteOrNot = '' + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + + +"@) + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + + [void]$htmlScopeInsights.AppendLine(@' + + + + + +'@) + } + + [void]$htmlScopeInsights.AppendLine(@" + + + + + + + + + + + + +"@) + $htmlScopeInsightsPolicySetAssignments = $null + $htmlScopeInsightsPolicySetAssignments = foreach ($policyAssignment in $policySetsAssigned | Sort-Object -Property Level, PolicyAssignmentId) { + if ($policyAssignment.PolicyType -eq 'Custom') { + $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') + } + else { + $policyName = $policyAssignment.PolicyName + } + @" + + + + + + + + + + + +"@ + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + @" + + + + + +"@ + } + @" + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsPolicySetAssignments) + [void]$htmlScopeInsights.AppendLine(@" + +
InheritanceScopeExcludedPolicySet DisplayNamePolicySetIdTypeCategoryALZParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotManaged IdentityAssignment DisplayNameAssignmentIdAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyName)$($policyAssignment.PolicyId)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyIsALZ)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages)$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentMI)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $(($policySetsAssigned).count) PolicySet assignments +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsPolicySetAssignments + + #PolicyAssignmentsLimit (Policy+PolicySet) + #region ScopeInsightsPolicyAssignmentsLimit + if ($mgOrSub -eq 'mg') { + $limit = $LimitPOLICYPolicyAssignmentsManagementGroup + } + if ($mgOrSub -eq 'sub') { + $limit = $LimitPOLICYPolicyAssignmentsSubscription + } + + if ($policiesAssignedAtScope -eq 0 -and $policySetsAssignedAtScope -eq 0) { + $faimage = "" + + [void]$htmlScopeInsights.AppendLine(@" + $faImage Policy Assignment Limit: 0/$limit +"@) + } + else { + if ($mgOrSub -eq 'mg') { + $scopePolicyAssignmentsLimit = $policyPolicyBaseQueryScopeInsights.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.MgId -eq $mgChild } ) + } + if ($mgOrSub -eq 'sub') { + $scopePolicyAssignmentsLimit = $policyPolicyBaseQueryScopeInsights.where( { $_.SubscriptionId -eq $subscriptionId } ) + } + + if ($scopePolicyAssignmentsLimit.PolicyAndPolicySetAssignmentAtScopeCount -gt (($limit) * $LimitCriticalPercentage / 100)) { + $faImage = "" + } + else { + $faimage = "" + } + [void]$htmlScopeInsights.AppendLine(@" + $faImage Policy Assignment Limit: $($scopePolicyAssignmentsLimit.PolicyAndPolicySetAssignmentAtScopeCount)/$($limit) +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsPolicyAssignmentsLimit + + #ScopedPolicies + #region ScopeInsightsScopedPolicies + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + $scopePolicies = $customPoliciesDetailed.where( { $_.PolicyDefinitionId -like "*/providers/Microsoft.Management/managementGroups/$mgChild/*" } ) + $scopePoliciesCount = ($scopePolicies).count + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + $scopePolicies = $customPoliciesDetailed.where( { $_.PolicyDefinitionId -like "*/subscriptions/$subscriptionId/*" } ) + $scopePoliciesCount = ($scopePolicies).count + } + + if ($scopePoliciesCount -gt 0) { + $tfCount = $scopePoliciesCount + $htmlTableId = "ScopeInsights_ScopedPolicies_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + if ($mgOrSub -eq 'mg') { + $LimitPOLICYPolicyScoped = $LimitPOLICYPolicyDefinitionsScopedManagementGroup + if ($scopePoliciesCount -gt (($LimitPOLICYPolicyScoped * $LimitCriticalPercentage) / 100)) { + $faIcon = "" + } + else { + $faIcon = "" + } + } + if ($mgOrSub -eq 'sub') { + $LimitPOLICYPolicyScoped = $LimitPOLICYPolicyDefinitionsScopedSubscription + if ($scopePoliciesCount -gt (($LimitPOLICYPolicyScoped * $LimitCriticalPercentage) / 100)) { + $faIcon = "" + } + else { + $faIcon = "" + } + } + + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlScopeInsightsScopedPolicies = $null + $htmlScopeInsightsScopedPolicies = foreach ($custompolicy in $scopePolicies | Sort-Object @{Expression = { $_.PolicyDisplayName } }, @{Expression = { $_.PolicyDefinitionId } }) { + if ($custompolicy.UsedInPolicySetsCount -gt 0) { + $customPolicyUsedInPolicySets = "$($customPolicy.UsedInPolicySetsCount) ($($customPolicy.UsedInPolicySets))" + } + else { + $customPolicyUsedInPolicySets = $($customPolicy.UsedInPolicySetsCount) + } + @" + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsScopedPolicies) + [void]$htmlScopeInsights.AppendLine(@" + +
Policy DisplayNamePolicyIdCategoryALZPolicy effectRole definitionsUnique assignmentsUsed in PolicySets
$($customPolicy.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyCategory -replace '<', '<' -replace '>', '>')$($customPolicy.ALZ)$($customPolicy.PolicyEffect)$($customPolicy.RoleDefinitions)$($customPolicy.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $scopePoliciesCount Custom Policy definitions scoped +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsScopedPolicies + + #ScopedPolicySets + #region ScopeInsightsScopedPolicySets + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + $scopePolicySets = $customPolicySetsDetailed.where( { $_.PolicySetDefinitionId -like "*/providers/Microsoft.Management/managementGroups/$mgChild/*" } ) + $scopePolicySetsCount = ($scopePolicySets).count + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + $scopePolicySets = $customPolicySetsDetailed.where( { $_.PolicySetDefinitionId -like "*/subscriptions/$subscriptionId/*" } ) + $scopePolicySetsCount = ($scopePolicySets).count + } + + if ($scopePolicySetsCount -gt 0) { + $tfCount = $scopePolicySetsCount + $htmlTableId = "ScopeInsights_ScopedPolicySets_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + if ($mgOrSub -eq 'mg') { + $LimitPOLICYPolicySetScoped = $LimitPOLICYPolicySetDefinitionsScopedManagementGroup + if ($scopePolicySetsCount -gt (($LimitPOLICYPolicySetScoped * $LimitCriticalPercentage) / 100)) { + $faIcon = "" + } + else { + $faIcon = "" + } + } + if ($mgOrSub -eq 'sub') { + $LimitPOLICYPolicySetScoped = $LimitPOLICYPolicySetDefinitionsScopedSubscription + if ($scopePolicySetsCount -gt (($LimitPOLICYPolicySetScoped * $LimitCriticalPercentage) / 100)) { + $faIcon = "" + } + else { + $faIcon = "" + } + } + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsScopedPolicySets = $null + $htmlScopeInsightsScopedPolicySets = foreach ($custompolicySet in $scopePolicySets | Sort-Object @{Expression = { $_.PolicySetDisplayName } }, @{Expression = { $_.PolicySetDefinitionId } }) { + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsScopedPolicySets) + [void]$htmlScopeInsights.AppendLine(@" + +
PolicySet DisplayNamePolicySetIdCategoryALZUnique assignmentsPolicies Used
$($custompolicySet.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($custompolicySet.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($custompolicySet.PolicySetCategory -replace '<', '<' -replace '>', '>')$($custompolicySet.ALZ)$($custompolicySet.UniqueAssignments -replace '<', '<' -replace '>', '>')$($custompolicySet.PoliciesUsed)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $scopePolicySetsCount Custom PolicySet definitions scoped +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsScopedPolicySets + + #BlueprintAssignments + #region ScopeInsightsBlueprintAssignments + if ($mgOrSub -eq 'sub') { + if ($blueprintsAssignedCount -gt 0) { + + if ($mgOrSub -eq 'mg') { + $htmlTableIdentifier = $mgChild + } + if ($mgOrSub -eq 'sub') { + $htmlTableIdentifier = $subscriptionId + } + $htmlTableId = "ScopeInsights_BlueprintAssignment_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlScopeInsightsBlueprintAssignments = $null + $htmlScopeInsightsBlueprintAssignments = foreach ($blueprintAssigned in $blueprintsAssigned) { + @" + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsBlueprintAssignments) + [void]$htmlScopeInsights.AppendLine(@" + +
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintIdBlueprint VersionBlueprint AssignmentId
$($blueprintAssigned.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintId -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintAssignmentVersion -replace '<', '<' -replace '>', '>')$($blueprintAssigned.BlueprintAssignmentId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $blueprintsAssignedCount Blueprints assigned +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + } + #endregion ScopeInsightsBlueprintAssignments + + #BlueprintsScoped + #region ScopeInsightsBlueprintsScoped + if ($blueprintsScopedCount -gt 0) { + $tfCount = $blueprintsScopedCount + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + } + $htmlTableId = "ScopeInsights_BlueprintScoped_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlScopeInsightsBlueprintsScoped = $null + $htmlScopeInsightsBlueprintsScoped = foreach ($blueprintScoped in $blueprintsScoped) { + @" + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsBlueprintsScoped) + [void]$htmlScopeInsights.AppendLine(@" + +
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintId
$($blueprintScoped.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintScoped.BlueprintId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $blueprintsScopedCount Blueprints scoped +"@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsBlueprintsScoped + + if ($mgOrSub -eq 'sub') { + #region ScopeInsightsClassicAdministrators + if ($htClassicAdministrators.($subscriptionId).ClassicAdministrators.Count -gt 0) { + $tfCount = $htClassicAdministrators.($subscriptionId).ClassicAdministrators.Count + $htmlTableId = "ScopeInsights_ClassicAdministrators_$($subscriptionId -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma + + + + + + + + +"@) + $htmlScopeInsightsClassicAdministrators = $null + $htmlScopeInsightsClassicAdministrators = foreach ($classicAdministrator in $htClassicAdministrators.($subscriptionId).ClassicAdministrators | Sort-Object -Property Role, Identity) { + @" + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsClassicAdministrators) + [void]$htmlScopeInsights.AppendLine(@" + +
RoleIdentity
$($classicAdministrator.Role)$($classicAdministrator.Identity)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@' + No Classic Administrators +'@) + } + [void]$htmlScopeInsights.AppendLine(@' + + +'@) + #endregion ScopeInsightsClassicAdministrators + } + + #RoleAssignments + #region ScopeInsightsRoleAssignments + if ($mgOrSub -eq 'mg') { + $SIDivContentClass = 'contentSIMG' + $htmlTableIdentifier = $mgChild + $LimitRoleAssignmentsScope = $LimitRBACRoleAssignmentsManagementGroup + + $rolesAssigned = [System.Collections.ArrayList]@() + $rolesAssignedCount = 0 + $rolesAssignedInheritedCount = 0 + $rolesAssignedUser = 0 + $rolesAssignedGroup = 0 + $rolesAssignedServicePrincipal = 0 + $rolesAssignedUnknown = 0 + $roleAssignmentsRelatedToPolicyCount = 0 + $roleSecurityFindingCustomRoleOwner = 0 + $roleSecurityFindingOwnerAssignmentSP = 0 + $rbacForThisManagementGroup = ($rbacAllGroupedByManagementGroup.where( { $_.name -eq $mgChild } )).group + foreach ($roleAssignment in $rbacForThisManagementGroup) { + if ([String]::IsNullOrEmpty($roleAssignment.subscriptionId)) { + $null = $rolesAssigned.Add($roleAssignment) + $rolesAssignedCount++ + if ($roleAssignment.Scope -notlike 'this*') { + $rolesAssignedInheritedCount++ + } + if ($roleAssignment.ObjectType -like 'User*') { + $rolesAssignedUser++ + } + if ($roleAssignment.ObjectType -eq 'Group') { + $rolesAssignedGroup++ + } + if ($roleAssignment.ObjectType -like 'SP*') { + $rolesAssignedServicePrincipal++ + } + if ($roleAssignment.ObjectType -eq 'Unknown') { + $rolesAssignedUnknown++ + } + if ($roleAssignment.RbacRelatedPolicyAssignment -ne 'none') { + $roleAssignmentsRelatedToPolicyCount++ + } + if ($roleAssignment.RoleSecurityCustomRoleOwner -eq 1) { + $roleSecurityFindingCustomRoleOwner++ + } + if ($roleAssignment.RoleSecurityOwnerAssignmentSP -eq 1) { + $roleSecurityFindingOwnerAssignmentSP++ + } + } + } + } + if ($mgOrSub -eq 'sub') { + $SIDivContentClass = 'contentSISub' + $htmlTableIdentifier = $subscriptionId + $LimitRoleAssignmentsScope = $htSubscriptionsRoleAssignmentLimit.($subscriptionId) + + $rolesAssigned = [System.Collections.ArrayList]@() + $rolesAssignedCount = 0 + $rolesAssignedInheritedCount = 0 + $rolesAssignedUser = 0 + $rolesAssignedGroup = 0 + $rolesAssignedServicePrincipal = 0 + $rolesAssignedUnknown = 0 + $roleAssignmentsRelatedToPolicyCount = 0 + $roleSecurityFindingCustomRoleOwner = 0 + $roleSecurityFindingOwnerAssignmentSP = 0 + $rbacForThisSubscription = ($rbacAllGroupedBySubscription.where( { $_.name -eq $subscriptionId } )).group + $rolesAssigned = foreach ($roleAssignment in $rbacForThisSubscription) { + + $roleAssignment + $rolesAssignedCount++ + if ($roleAssignment.Scope -notlike 'this*') { + $rolesAssignedInheritedCount++ + } + if ($roleAssignment.ObjectType -like 'User*') { + $rolesAssignedUser++ + } + if ($roleAssignment.ObjectType -eq 'Group') { + $rolesAssignedGroup++ + } + if ($roleAssignment.ObjectType -like 'SP*') { + $rolesAssignedServicePrincipal++ + } + if ($roleAssignment.ObjectType -eq 'Unknown') { + $rolesAssignedUnknown++ + } + if ($roleAssignment.RbacRelatedPolicyAssignment -ne 'none') { + $roleAssignmentsRelatedToPolicyCount++ + } + if ($roleAssignment.RoleSecurityCustomRoleOwner -eq 1) { + $roleSecurityFindingCustomRoleOwner++ + } + if ($roleAssignment.RoleSecurityOwnerAssignmentSP -eq 1) { + $roleSecurityFindingOwnerAssignmentSP++ + } + } + } + + $rolesAssignedAtScopeCount = $rolesAssignedCount - $rolesAssignedInheritedCount + + if (($rolesAssigned).count -gt 0) { + $tfCount = ($rolesAssigned).count + $htmlTableId = "ScopeInsights_RoleAssignments_$($htmlTableIdentifier -replace '\(','_' -replace '\)','_' -replace '-','_' -replace '\.','_')" + $randomFunctionName = "func_$htmlTableId" + $noteOrNot = '' + [void]$htmlScopeInsights.AppendLine(@" + +
+   Download CSV semicolon | comma
+  *Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience + + + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlScopeInsightsRoleAssignments = $null + $htmlScopeInsightsRoleAssignments = foreach ($roleAssignment in ($rolesAssigned | Sort-Object -Property Level, MgName, MgId, SubscriptionName, SubscriptionId, Scope, Role, RoleId, ObjectId, RoleAssignmentId)) { + @" + + + + + + + + + + + + + + + + + + + +"@ + } + [void]$htmlScopeInsights.AppendLine($htmlScopeInsightsRoleAssignments) + [void]$htmlScopeInsights.AppendLine(@" + +
ScopeRoleRoleIdRole TypeDataCan do Role assignmentIdentity DisplaynameIdentity SignInNameIdentity ObjectIdIdentity TypeApplicabilityApplies through membership Group DetailsRole AssignmentIdRelated Policy Assignment $noteOrNotCreatedOnCreatedBy
$($roleAssignment.Scope)$($roleAssignment.Role)$($roleAssignment.RoleId)$($roleAssignment.RoleType)$($roleAssignment.RoleDataRelated)$($roleAssignment.RoleCanDoRoleAssignments)$($roleAssignment.ObjectDisplayName)$($roleAssignment.ObjectSignInName)$($roleAssignment.ObjectId)$($roleAssignment.ObjectType)$($roleAssignment.AssignmentType)$($roleAssignment.AssignmentInheritFrom)$($roleAssignment.GroupMembersCount)$($roleAssignment.RoleAssignmentId)$($roleAssignment.rbacRelatedPolicyAssignment)$($roleAssignment.CreatedOn)$($roleAssignment.CreatedBy)
+
+ +"@) + } + else { + [void]$htmlScopeInsights.AppendLine(@" + $(($rbacAll).count) Role assignments + +"@) + } + + [void]$htmlScopeInsights.AppendLine(@' + +'@) + #endregion ScopeInsightsRoleAssignments + + + if (-not $NoScopeInsights) { + $script:html += $htmlScopeInsights + } + + if (-not $NoSingleSubscriptionOutput) { + if ($mgOrSub -eq 'sub') { + $htmlThisSubSingleOutput = $htmlSubscriptionOnlyStart + $htmlThisSubSingleOutput += $htmlScopeInsights + $htmlThisSubSingleOutput += $htmlSubscriptionOnlyEnd + $htmlThisSubSingleOutput | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)$($DirectorySeparatorChar)$($fileName)_$($subscriptionId).html" -Encoding utf8 -Force + $htmlThisSubSingleOutput = $null + } + } + + if (-not $NoScopeInsights) { + if ($scopescnter % 50 -eq 0) { + $script:scopescnter = 0 + Write-Host ' append file duration: '(Measure-Command { $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force }).TotalSeconds 'seconds' + $script:html = $null + } + } + + if ($scopescnter % 50 -eq 0) { + showMemoryUsage + } + +} +function processStorageAccountAnalysis { + $start = Get-Date + Write-Host 'Processing Storage Account Analysis' + $storageAccountsCount = $storageAccounts.count + if ($storageAccountsCount -gt 0) { + Write-Host " Executing Storage Account Analysis for $storageAccountsCount Storage Accounts" + createBearerToken -AzAPICallConfiguration $azapicallconf -targetEndPoint 'Storage' + + $htSACost = @{} + if ($DoAzureConsumption -eq $true) { + $saConsumptionByResourceId = $allConsumptionData.where({ $_.resourceType -eq 'microsoft.storage/storageaccounts' }) | Group-Object -Property resourceid + + foreach ($sa in $saConsumptionByResourceId) { + $htSACost.($sa.Name) = @{} + $htSACost.($sa.Name).meterCategoryAll = ($sa.Group.MeterCategory | Sort-Object) -join ', ' + $htSACost.($sa.Name).costAll = ($sa.Group.PreTaxCost | Measure-Object -Sum).Sum #[decimal]($sa.Group.PreTaxCost | Measure-Object -Sum).Sum + $htSACost.($sa.Name).currencyAll = ($sa.Group.Currency | Sort-Object -Unique) -join ', ' + foreach ($costentry in $sa.Group) { + $htSACost.($sa.Name)."cost_$($costentry.MeterCategory)" = $costentry.PreTaxCost + $htSACost.($sa.Name)."currency_$($costentry.MeterCategory)" = $costentry.Currency + } + } + } + + $storageAccounts | ForEach-Object -Parallel { + $storageAccount = $_ + $azAPICallConf = $using:azAPICallConf + $arrayStorageAccountAnalysisResults = $using:arrayStorageAccountAnalysisResults + $htAllSubscriptionsFromAPI = $using:htAllSubscriptionsFromAPI + $htSubscriptionsMgPath = $using:htSubscriptionsMgPath + $htSubscriptionTags = $using:htSubscriptionTags + $CSVDelimiterOpposite = $using:CSVDelimiterOpposite + $htSACost = $using:htSACost + $StorageAccountAccessAnalysisSubscriptionTags = $using:StorageAccountAccessAnalysisSubscriptionTags + $StorageAccountAccessAnalysisStorageAccountTags = $using:StorageAccountAccessAnalysisStorageAccountTags + $listContainersSuccess = 'n/a' + $containersCount = 'n/a' + $arrayContainers = @() + $arrayContainersAnonymousContainer = @() + $arrayContainersAnonymousBlob = @() + $staticWebsitesState = 'n/a' + $webSiteResponds = 'n/a' + + $subscriptionId = ($storageAccount.SA.id -split '/')[2] + $resourceGroupName = ($storageAccount.SA.id -split '/')[4] + $subDetails = $htAllSubscriptionsFromAPI.($subscriptionId).subDetails + + Write-Host "Processing Storage Account '$($storageAccount.SA.name)' - Subscription: '$($subDetails.displayName)' ($subscriptionId) [$($subDetails.subscriptionPolicies.quotaId)]" + + if ($storageAccount.SA.Properties.primaryEndpoints.blob) { + + $urlServiceProps = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?restype=service&comp=properties" + $saProperties = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlServiceProps -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get restype=service&comp=properties" -saResourceGroupName $resourceGroupName -unhandledErrorAction Continue + if ($saProperties) { + if ($saProperties -eq 'AuthorizationFailure' -or $saProperties -eq 'AuthorizationPermissionDenied' -or $saProperties -eq 'ResourceUnavailable' -or $saProperties -eq 'AuthorizationPermissionMismatch' ) { + if ($saProperties -eq 'ResourceUnavailable') { + $staticWebsitesState = $saProperties + } + } + else { + try { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions + + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { + if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { + $staticWebsitesState = $true + } + else { + $staticWebsitesState = $false + } + } + } + catch { + Write-Host "XMLSAPropertiesFailed: Subscription: $($subDetails.displayName) ($subscriptionId) - Storage Account: $($storageAccount.SA.name)" + Write-Host $($saProperties.ForEach({ [char]$_ }) -join '') -ForegroundColor Cyan + } + } + } + + $urlCompList = "$($storageAccount.SA.Properties.primaryEndpoints.blob)?comp=list" + $listContainers = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $urlCompList -method 'GET' -listenOn 'Content' -currentTask "$($storageAccount.SA.name) get comp=list" -unhandledErrorAction Continue + if ($listContainers) { + if ($listContainers -eq 'AuthorizationFailure' -or $listContainers -eq 'AuthorizationPermissionDenied' -or $listContainers -eq 'ResourceUnavailable' -or $listContainers -eq 'AuthorizationPermissionMismatch') { + if ($listContainers -eq 'ResourceUnavailable') { + $listContainersSuccess = $listContainers + } + else { + $listContainersSuccess = $false + } + } + else { + $listContainersSuccess = $true + } + + if ($listContainersSuccess -eq $true) { + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions + + $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count + + foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { + $arrayContainers += $container.Name + if ($container.Name -eq '$web' -and $staticWebsitesState) { + if ($storageAccount.SA.properties.primaryEndpoints.web) { + try { + $testStaticWebsiteResponse = Invoke-WebRequest -Uri $storageAccount.SA.properties.primaryEndpoints.web -Method 'HEAD' + $webSiteResponds = $true + } + catch { + $webSiteResponds = $false + } + } + } + + if ($container.Properties.PublicAccess) { + if ($container.Properties.PublicAccess -eq 'blob') { + $arrayContainersAnonymousBlob += $container.Name + } + if ($container.Properties.PublicAccess -eq 'container') { + $arrayContainersAnonymousContainer += $container.Name + } + } + } + } + } + } + + $allowSharedKeyAccess = $storageAccount.SA.properties.allowSharedKeyAccess + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowSharedKeyAccess)) { + $allowSharedKeyAccess = 'likely True' + } + $requireInfrastructureEncryption = $storageAccount.SA.properties.encryption.requireInfrastructureEncryption + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.encryption.requireInfrastructureEncryption)) { + $requireInfrastructureEncryption = 'likely False' + } + + $arrayResourceAccessRules = [System.Collections.ArrayList]@() + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules) { + if ($storageAccount.SA.properties.networkAcls.resourceAccessRules.count -gt 0) { + foreach ($resourceAccessRule in $storageAccount.SA.properties.networkAcls.resourceAccessRules) { + + $resourceAccessRuleResourceIdSplitted = $resourceAccessRule.resourceId -split '/' + $resourceType = "$($resourceAccessRuleResourceIdSplitted[6])/$($resourceAccessRuleResourceIdSplitted[7])" + + [regex]$regex = '\*+' + #$resourceAccessRule.resourceId + switch ($regex.matches($resourceAccessRule.resourceId).count) { + { $_ -eq 1 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resourceGroup' + sort = 3 + }) + } + { $_ -eq 2 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'subscription' + sort = 2 + }) + } + { $_ -eq 3 } { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'tenant' + sort = 1 + }) + } + default { + $null = $arrayResourceAccessRules.Add([PSCustomObject]@{ + resourcetype = $resourceType + range = 'resource' + resource = $resourceAccessRule.resourceId + sort = 0 + }) + } + } + } + } + } + $resourceAccessRulesCount = $arrayResourceAccessRules.count + if ($resourceAccessRulesCount -eq 0) { + $resourceAccessRules = '' + } + else { + $ht = @{} + foreach ($accessRulePerRange in $arrayResourceAccessRules | Group-Object -Property range | Sort-Object -Property Name -Descending) { + + if ($accessRulePerRange.Name -eq 'resource') { + $arrayResources = @() + foreach ($resource in $accessRulePerRange.Group.resource | Sort-Object) { + $arrayResources += $resource + } + $ht.($accessRulePerRange.Name) = [array]($arrayResources) + } + else { + $arrayResourceTypes = @() + foreach ($resourceType in $accessRulePerRange.Group.resourceType | Sort-Object) { + $arrayResourceTypes += $resourceType + } + $ht.($accessRulePerRange.Name) = [array]($arrayResourceTypes) + } + } + $resourceAccessRules = $ht | ConvertTo-Json + } + + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.publicNetworkAccess)) { + $publicNetworkAccess = 'likely Enabled' + } + else { + $publicNetworkAccess = $storageAccount.SA.properties.publicNetworkAccess + } + + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowedCopyScope)) { + $allowedCopyScope = 'From any Storage Account' + } + else { + $allowedCopyScope = $storageAccount.SA.properties.allowedCopyScope + } + + if ([string]::IsNullOrWhiteSpace($storageAccount.SA.properties.allowCrossTenantReplication)) { + if ($allowedCopyScope -ne 'From any Storage Account') { + $allowCrossTenantReplication = "likely False (allowedCopyScope=$allowedCopyScope)" + } + else { + $allowCrossTenantReplication = 'likely True' + } + } + else { + $allowCrossTenantReplication = $storageAccount.SA.properties.allowCrossTenantReplication + } + + if ($storageAccount.SA.properties.dnsEndpointType) { + $dnsEndpointType = $storageAccount.SA.properties.dnsEndpointType + } + else { + $dnsEndpointType = 'standard' + } + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($htSACost.($storageAccount.SA.id)) { + $hlpCost = $htSACost.($storageAccount.SA.id) + $saCost = $hlpCost.costAll + $saCostCurrency = $hlpCost.currencyAll + $saCostMeterCategories = $hlpCost.meterCategoryAll + } + else { + $saCost = 'n/a' + $saCostCurrency = 'n/a' + $saCostMeterCategories = 'n/a' + } + } + else { + $saCost = '' + $saCostCurrency = '' + $saCostMeterCategories = '' + } + + $temp = [System.Collections.ArrayList]@() + $null = $temp.Add([PSCustomObject]@{ + storageAccount = $storageAccount.SA.name + kind = $storageAccount.SA.kind + skuName = $storageAccount.SA.sku.name + skuTier = $storageAccount.SA.sku.tier + location = $storageAccount.SA.location + creationTime = $storageAccount.SA.properties.creationTime + allowBlobPublicAccess = $storageAccount.SA.properties.allowBlobPublicAccess + publicNetworkAccess = $publicNetworkAccess + SubscriptionId = $subscriptionId + SubscriptionName = $subDetails.displayName + subscriptionQuotaId = $subDetails.subscriptionPolicies.quotaId + subscriptionMGPath = $htSubscriptionsMgPath.($subscriptionId).path -join '/' + resourceGroup = $resourceGroupName + networkAclsdefaultAction = $storageAccount.SA.properties.networkAcls.defaultAction + staticWebsitesState = $staticWebsitesState + staticWebsitesResponse = $webSiteResponds + containersCanBeListed = $listContainersSuccess + containersCount = $containersCount + containers = $arrayContainers -join "$CSVDelimiterOpposite " + containersAnonymousContainerCount = $arrayContainersAnonymousContainer.Count + containersAnonymousContainer = $arrayContainersAnonymousContainer -join "$CSVDelimiterOpposite " + containersAnonymousBlobCount = $arrayContainersAnonymousBlob.Count + containersAnonymousBlob = $arrayContainersAnonymousBlob -join "$CSVDelimiterOpposite " + ipRulesCount = $storageAccount.SA.properties.networkAcls.ipRules.Count + ipRulesIPAddressList = ($storageAccount.SA.properties.networkAcls.ipRules.value | Sort-Object) -join "$CSVDelimiterOpposite " + virtualNetworkRulesCount = $storageAccount.SA.properties.networkAcls.virtualNetworkRules.Count + virtualNetworkRulesList = ($storageAccount.SA.properties.networkAcls.virtualNetworkRules.Id | Sort-Object) -join "$CSVDelimiterOpposite " + resourceAccessRulesCount = $resourceAccessRulesCount + resourceAccessRules = $resourceAccessRules + bypass = ($storageAccount.SA.properties.networkAcls.bypass | Sort-Object) -join "$CSVDelimiterOpposite " + supportsHttpsTrafficOnly = $storageAccount.SA.properties.supportsHttpsTrafficOnly + minimumTlsVersion = $storageAccount.SA.properties.minimumTlsVersion + allowSharedKeyAccess = $allowSharedKeyAccess + requireInfrastructureEncryption = $requireInfrastructureEncryption + allowedCopyScope = $allowedCopyScope + allowCrossTenantReplication = $allowCrossTenantReplication + dnsEndpointType = $dnsEndpointType + usedCapacity = $storageAccount.SAUsedCapacity + cost = $saCost + metercategory = $saCostMeterCategories + curreny = $saCostCurrency + }) + + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + foreach ($subTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisSubscriptionTags) { + if ($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htSubscriptionTags.($subscriptionId).$subTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SubTag_$subTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + if ($storageAccount.SA.tags) { + $htAllSATags = @{} + foreach ($saTagName in ($storageAccount.SA.tags | Get-Member).where({ $_.MemberType -eq 'NoteProperty' }).Name) { + $htAllSATags.$saTagName = $storageAccount.SA.tags.$saTagName + } + } + foreach ($saTag4StorageAccountAccessAnalysis in $StorageAccountAccessAnalysisStorageAccountTags) { + if ($htAllSATags.$saTag4StorageAccountAccessAnalysis) { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue $($htAllSATags.$saTag4StorageAccountAccessAnalysis) + } + else { + $temp | Add-Member -NotePropertyName "SATag_$saTag4StorageAccountAccessAnalysis" -NotePropertyValue 'n/a' + } + } + } + + $null = $script:arrayStorageAccountAnalysisResults.AddRange($temp) + + } -ThrottleLimit $ThrottleLimit + } + else { + Write-Host ' No Storage Accounts present' + } + + $end = Get-Date + Write-Host " Processing Storage Account Analysis duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" +} +function processTenantSummary() { + Write-Host ' Building TenantSummary' + showMemoryUsage + if ($getMgParentName -eq 'Tenant Root') { + $scopeNamingSummary = 'Tenant wide' + } + else { + $scopeNamingSummary = "ManagementGroup '$ManagementGroupId' and descendants wide" + } + + #region tenantSummaryPre + $startRoleAssignmentsAllPre = Get-Date + $roleAssignmentsallCount = ($rbacBaseQuery).count + Write-Host " processing (pre) TenantSummary RoleAssignments (all $roleAssignmentsallCount)" + + #region RelatedPolicyAssignments + $startRelatedPolicyAssignmentsAll = Get-Date + $htRoleAssignmentRelatedPolicyAssignments = @{} + $htOrphanedSPMI = @{} + foreach ($roleAssignmentIdUnique in $roleAssignmentsUniqueById) { + + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId) = @{} + + if ($htManagedIdentityForPolicyAssignment.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { + $hlpPolicyAssignmentId = ($htManagedIdentityForPolicyAssignment.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId).policyAssignmentId).ToLower() + if (-not $htCacheAssignmentsPolicy.($hlpPolicyAssignmentId)) { + if ($ManagementGroupId -eq $azAPICallConf['checkContext'].Tenant.Id) { + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not ($htCacheAssignmentsPolicyOnResourceGroupsAndResources).($hlpPolicyAssignmentId)) { + Write-Host " !Relict detected: SP MI: $($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) - PolicyAssignmentId: $hlpPolicyAssignmentId" + if (-not $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { + $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) = @{} + } + } + } + else { + Write-Host " !Relict detected: SP MI: $($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) - PolicyAssignmentId: $hlpPolicyAssignmentId" + if (-not $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId)) { + $htOrphanedSPMI.($roleAssignmentIdUnique.RoleAssignmentIdentityObjectId) = @{} + } + } + } + } + else { + $temp0000000000 = $htCacheAssignmentsPolicy.($hlpPolicyAssignmentId) + $policyAssignmentId = ($temp0000000000.Assignment.id).Tolower() + $policyDefinitionId = ($temp0000000000.Assignment.properties.policyDefinitionId).Tolower() + + + #builtin + if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policy*') { + #policy + if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policyDefinitions/*') { + $LinkOrNotLinkToAzAdvertizer = ($htCacheDefinitionsPolicy).($policyDefinitionId).LinkToAzAdvertizer + } + #policySet + if ($policyDefinitionId -like '/providers/Microsoft.Authorization/policySetDefinitions/*') { + $LinkOrNotLinkToAzAdvertizer = ($htCacheDefinitionsPolicySet).($policyDefinitionId).LinkToAzAdvertizer + } + } + else { + #policy + if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { + $policyDisplayName = ($htCacheDefinitionsPolicy).($policyDefinitionId).DisplayName + + } + #policySet + if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { + $policyDisplayName = ($htCacheDefinitionsPolicySet).($policyDefinitionId).DisplayName + + } + + $LinkOrNotLinkToAzAdvertizer = "$($policyDisplayName -replace '<', '<' -replace '>', '>')" + } + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignment = "$($policyAssignmentId) ($LinkOrNotLinkToAzAdvertizer)" + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignmentClear = "$($policyAssignmentId) ($policyDisplayName)" + } + } + else { + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignment = 'none' + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).relatedPolicyAssignmentClear = 'none' + } + + if ($roleAssignmentIdUnique.RoleIsCustom -eq 'FALSE') { + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleType = 'Builtin' + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleWithWithoutLinkToAzAdvertizer = ($htCacheDefinitionsRole).($roleAssignmentIdUnique.RoleDefinitionId).LinkToAzAdvertizer + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleClear = $roleAssignmentIdUnique.RoleDefinitionName + } + else { + + if ($roleAssigned.RoleSecurityCustomRoleOwner -eq 1) { + $roletype = " Custom" + } + else { + $roleType = 'Custom' + } + + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleType = $roleType + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleWithWithoutLinkToAzAdvertizer = $roleAssignmentIdUnique.RoleDefinitionName + $htRoleAssignmentRelatedPolicyAssignments.($roleAssignmentIdUnique.RoleAssignmentId).roleClear = $roleAssignmentIdUnique.RoleDefinitionName + } + } + $endRelatedPolicyAssignmentsAll = Get-Date + Write-Host " RelatedPolicyAssignmentsAll duration: $((New-TimeSpan -Start $startRelatedPolicyAssignmentsAll -End $endRelatedPolicyAssignmentsAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startRelatedPolicyAssignmentsAll -End $endRelatedPolicyAssignmentsAll).TotalSeconds) seconds)" + #endregion RelatedPolicyAssignments + + #region createRBACAll + $cnter = 0 + $script:rbacAll = [System.Collections.ArrayList]@() + $startCreateRBACAll = Get-Date + foreach ($rbac in $rbacBaseQuery) { + $cnter++ + if ($cnter % 1000 -eq 0) { + $etappeRoleAssignmentsAll = Get-Date + Write-Host " $cnter of $roleAssignmentsallCount RoleAssignments processed; $((New-TimeSpan -Start $startRoleAssignmentsAllPre -End $etappeRoleAssignmentsAll).TotalSeconds) seconds" + } + $scope = $null + + if ($rbac.RoleAssignmentPIM -eq 'true') { + $pim = $true + $pimAssignmentType = $rbac.RoleAssignmentPIMAssignmentType + $pimSlotStart = [string]$($rbac.RoleAssignmentPIMSlotStart) + $pimSlotEnd = [string]$($rbac.RoleAssignmentPIMSlotEnd) + } + else { + $pim = $false + $pimAssignmentType = '' + $pimSlotStart = '' + $pimSlotEnd = '' + } + + if ($rbac.RoleAssignmentId -like '/providers/Microsoft.Management/managementGroups/*') { + $scopeTenOrMgOrSubOrRGOrRes = 'Mg' + if (-not [String]::IsNullOrEmpty($rbac.SubscriptionId)) { + $scope = "inherited $($rbac.RoleAssignmentScopeName)" + } + else { + if (($rbac.RoleAssignmentScopeName) -eq $rbac.MgId) { + $scope = 'thisScope MG' + } + else { + $scope = "inherited $($rbac.RoleAssignmentScopeName)" + } + } + } + + if ($rbac.RoleAssignmentId -like '/subscriptions/*') { + $scope = 'thisScope Sub' + $scopeTenOrMgOrSubOrRGOrRes = 'Sub' + } + + if ($rbac.RoleAssignmentId -like '/subscriptions/*/resourcegroups/*') { + $scope = 'thisScope Sub RG' + $scopeTenOrMgOrSubOrRGOrRes = 'RG' + } + + if ($rbac.RoleAssignmentId -like '/subscriptions/*/resourcegroups/*/providers/*/providers/*') { + $scope = 'thisScope Sub RG Res' + $scopeTenOrMgOrSubOrRGOrRes = 'Res' + } + + if ($rbac.RoleAssignmentId -like '/providers/Microsoft.Authorization/roleAssignments/*') { + $scope = 'inherited Tenant' + $scopeTenOrMgOrSubOrRGOrRes = 'Ten' + } + + $objectTypeUserType = '' + if ($rbac.RoleAssignmentIdentityObjectType -eq 'User') { + if ($htUserTypesGuest.($rbac.RoleAssignmentIdentityObjectId)) { + $objectTypeUserType = 'Guest' + } + else { + $objectTypeUserType = 'Member' + } + } + + if (-not [string]::IsNullOrEmpty($rbac.RoleDataActions) -or -not [string]::IsNullOrEmpty($rbac.RoleNotDataActions)) { + $roleManageData = 'true' + } + else { + $roleManageData = 'false' + } + + $hlpRoleAssignmentRelatedPolicyAssignments = $htRoleAssignmentRelatedPolicyAssignments.($rbac.RoleAssignmentId) + + if (-not $NoAADGroupsResolveMembers) { + if ($rbac.RoleAssignmentIdentityObjectType -eq 'Group') { + + $grpHlpr = $htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId) + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $rbac.Level + RoleAssignmentId = $rbac.RoleAssignmentId + RoleAssignmentPIMRelated = $pim + RoleAssignmentPIMAssignmentType = $pimAssignmentType + RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart + RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd + CreatedBy = $rbac.RoleAssignmentCreatedBy + CreatedOn = $rbac.RoleAssignmentCreatedOn + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $rbac.MgId + MgName = $rbac.MgName + MgParentId = $rbac.MgParentId + MgParentName = $rbac.MgParentName + SubscriptionId = $rbac.SubscriptionId + SubscriptionName = $rbac.Subscription + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes + RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName + RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG + RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes + Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer + RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear + RoleId = $rbac.RoleDefinitionId + RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType + RoleDataRelated = $roleManageData + AssignmentType = 'direct' + AssignmentInheritFrom = '' + GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" + ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname + ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName + ObjectId = $rbac.RoleAssignmentIdentityObjectId + ObjectType = $rbac.RoleAssignmentIdentityObjectType + RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment + RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear + RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments + }) + + + if ($grpHlpr.MembersAllCount -gt 0) { + + if ($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount -le $AADGroupMembersLimit) { + + foreach ($groupmember in $htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAll) { + if ($groupmember.'@odata.type' -eq '#microsoft.graph.user') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $true) { + $grpMemberDisplayName = 'scrubbed' + $grpMemberSignInName = 'scrubbed' + } + else { + $grpMemberDisplayName = $groupmember.displayName + $grpMemberSignInName = $groupmember.userPrincipalName + } + $grpMemberId = $groupmember.Id + $grpMemberType = 'User' + $grpMemberUserType = '' + + if ($htUserTypesGuest.($grpMemberId)) { + $grpMemberUserType = 'Guest' + } + else { + $grpMemberUserType = 'Member' + } + + $identityTypeFull = "$grpMemberType $grpMemberUserType" + } + if ($groupmember.'@odata.type' -eq '#microsoft.graph.group') { + $grpMemberDisplayName = $groupmember.displayName + $grpMemberSignInName = 'n/a' + $grpMemberId = $groupmember.Id + $grpMemberType = 'Group' + $grpMemberUserType = '' + $identityTypeFull = "$grpMemberType" + } + if ($groupmember.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + $grpMemberDisplayName = $groupmember.appDisplayName + $grpMemberSignInName = 'n/a' + $grpMemberId = $groupmember.Id + $grpMemberType = 'ServicePrincipal' + $grpMemberUserType = '' + $identityType = $htServicePrincipals.($grpMemberId).spTypeConcatinated + $identityTypeFull = "$identityType" + } + + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $rbac.Level + RoleAssignmentId = $rbac.RoleAssignmentId + RoleAssignmentPIMRelated = $pim + RoleAssignmentPIMAssignmentType = $pimAssignmentType + RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart + RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd + CreatedBy = $rbac.RoleAssignmentCreatedBy + CreatedOn = $rbac.RoleAssignmentCreatedOn + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $rbac.MgId + MgName = $rbac.MgName + MgParentId = $rbac.MgParentId + MgParentName = $rbac.MgParentName + SubscriptionId = $rbac.SubscriptionId + SubscriptionName = $rbac.Subscription + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes + RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName + RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG + RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes + Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer + RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear + RoleId = $rbac.RoleDefinitionId + RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType + RoleDataRelated = $roleManageData + AssignmentType = 'indirect' + AssignmentInheritFrom = "$($rbac.RoleAssignmentIdentityDisplayname) ($($rbac.RoleAssignmentIdentityObjectId))" + GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" + ObjectDisplayName = $grpMemberDisplayName + ObjectSignInName = $grpMemberSignInName + ObjectId = $grpMemberId + ObjectType = $identityTypeFull + RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment + RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear + RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments + }) + } + } + else { + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $rbac.Level + RoleAssignmentId = $rbac.RoleAssignmentId + RoleAssignmentPIMRelated = $pim + RoleAssignmentPIMAssignmentType = $pimAssignmentType + RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart + RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd + CreatedBy = $rbac.RoleAssignmentCreatedBy + CreatedOn = $rbac.RoleAssignmentCreatedOn + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $rbac.MgId + MgName = $rbac.MgName + MgParentId = $rbac.MgParentId + MgParentName = $rbac.MgParentName + SubscriptionId = $rbac.SubscriptionId + SubscriptionName = $rbac.Subscription + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes + RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName + RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG + RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes + Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer + RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear + RoleId = $rbac.RoleDefinitionId + RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType + RoleDataRelated = $roleManageData + AssignmentType = 'indirect' + AssignmentInheritFrom = "$($rbac.RoleAssignmentIdentityDisplayname) ($($rbac.RoleAssignmentIdentityObjectId))" + GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" + ObjectDisplayName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectSignInName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectId = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectType = 'unresolved' + RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment + RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear + RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments + }) + } + } + + } + else { + + if ($rbac.RoleAssignmentIdentityObjectType -eq 'ServicePrincipal') { + $identityType = $htServicePrincipals.($rbac.RoleAssignmentIdentityObjectId).spTypeConcatinated + $identityTypeFull = $identityType + } + elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Unknown') { + $identityTypeFull = 'Unknown' + } + else { + #user + $identityType = $rbac.RoleAssignmentIdentityObjectType + $identityTypeFull = "$identityType $objectTypeUserType" + } + + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $rbac.Level + RoleAssignmentId = $rbac.RoleAssignmentId + RoleAssignmentPIMRelated = $pim + RoleAssignmentPIMAssignmentType = $pimAssignmentType + RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart + RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd + CreatedBy = $rbac.RoleAssignmentCreatedBy + CreatedOn = $rbac.RoleAssignmentCreatedOn + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $rbac.MgId + MgName = $rbac.MgName + MgParentId = $rbac.MgParentId + MgParentName = $rbac.MgParentName + SubscriptionId = $rbac.SubscriptionId + SubscriptionName = $rbac.Subscription + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes + RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName + RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG + RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes + Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer + RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear + RoleId = $rbac.RoleDefinitionId + RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType + RoleDataRelated = $roleManageData + AssignmentType = 'direct' + AssignmentInheritFrom = '' + GroupMembersCount = '' + ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname + ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName + ObjectId = $rbac.RoleAssignmentIdentityObjectId + ObjectType = $identityTypeFull + RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment + RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear + RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments + }) + } + } + else { + + if ($rbac.RoleAssignmentIdentityObjectType -eq 'ServicePrincipal') { + $identityType = $htServicePrincipals.($rbac.RoleAssignmentIdentityObjectId).spTypeConcatinated + $identityTypeFull = $identityType + } + elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Unknown') { + $identityTypeFull = 'Unknown' + } + elseif ($rbac.RoleAssignmentIdentityObjectType -eq 'Group') { + $identityTypeFull = 'Group' + } + else { + #user + $identityType = $rbac.RoleAssignmentIdentityObjectType + $identityTypeFull = "$identityType $objectTypeUserType" + } + + #noaadgroupmemberresolve + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $rbac.Level + RoleAssignmentId = $rbac.RoleAssignmentId + RoleAssignmentPIMRelated = $pim + RoleAssignmentPIMAssignmentType = $pimAssignmentType + RoleAssignmentPIMAssignmentSlotStart = $pimSlotStart + RoleAssignmentPIMAssignmentSlotEnd = $pimSlotEnd + CreatedBy = $rbac.RoleAssignmentCreatedBy + CreatedOn = $rbac.RoleAssignmentCreatedOn + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $rbac.MgId + MgName = $rbac.MgName + MgParentId = $rbac.MgParentId + MgParentName = $rbac.MgParentName + SubscriptionId = $rbac.SubscriptionId + SubscriptionName = $rbac.Subscription + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $scopeTenOrMgOrSubOrRGOrRes + RoleAssignmentScopeName = $rbac.RoleAssignmentScopeName + RoleAssignmentScopeRG = $rbac.RoleAssignmentScopeRG + RoleAssignmentScopeRes = $rbac.RoleAssignmentScopeRes + Role = $hlpRoleAssignmentRelatedPolicyAssignments.roleWithWithoutLinkToAzAdvertizer + RoleClear = $hlpRoleAssignmentRelatedPolicyAssignments.roleClear + RoleId = $rbac.RoleDefinitionId + RoleType = $hlpRoleAssignmentRelatedPolicyAssignments.roleType + RoleDataRelated = $roleManageData + AssignmentType = 'direct' + AssignmentInheritFrom = '' + GroupMembersCount = '' + ObjectDisplayName = $rbac.RoleAssignmentIdentityDisplayname + ObjectSignInName = $rbac.RoleAssignmentIdentitySignInName + ObjectId = $rbac.RoleAssignmentIdentityObjectId + ObjectType = $identityTypeFull + RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment + RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear + RoleSecurityCustomRoleOwner = $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $rbac.RoleCanDoRoleAssignments + }) + } + } + #endregion createRBACAll + + #region PIMEligible + if (-not $NoPIMEligibility) { + $startPIMEnrichment = Get-Date + Write-Host ' Processing PIMEnrichment' + $PIMEligibleEnriched = [System.Collections.ArrayList]@() + #$tfCountCnt = 0 + foreach ($PIMEligible in $arrayPIMEligible) { + #$tfCountCnt++ + if ($PIMEligible.RoleType -eq 'BuiltInRole') { + $roleName = "$($PIMEligible.RoleName)" + } + else { + $roleName = $PIMEligible.RoleName + } + $null = $PIMEligibleEnriched.Add([PSCustomObject]@{ + Scope = $PIMEligible.ScopeType + ScopeId = $PIMEligible.ScopeId + ScopeName = $PIMEligible.ScopeDisplayName + ManagementGroupId = $PIMEligible.ManagementGroupId + ManagementGroupDisplayName = $PIMEligible.ManagementGroupDisplayName + SubscriptionId = $PIMEligible.SubscriptionId + SubscriptionDisplayName = $PIMEligible.SubscriptionDisplayName + MgPath = $PIMEligible.MgPath -join '/' + MgLevel = $PIMEligible.MgLevel + Role = $roleName + RoleClear = $PIMEligible.RoleName + RoleId = $PIMEligible.RoleId + RoleIdGuid = $PIMEligible.RoleIdGuid + RoleType = $PIMEligible.RoleType + IdentityObjectId = $PIMEligible.IdentityObjectId + IdentityDisplayName = $PIMEligible.IdentityDisplayName + IdentitySignInName = $PIMEligible.IdentityPrincipalName + IdentityType = $PIMEligible.IdentityType + IdentityApplicability = 'direct' + AppliesThrough = '' + PIMEligibilityId = $PIMEligible.PIMId + PIMEligibility = $PIMEligible.PIMInheritance + PIMEligibilityInheritedFrom = $PIMEligible.PIMInheritedFrom + PIMEligibilityInheritedFromClear = $PIMEligible.PIMInheritedFromClear + PIMEligibilityStartDateTime = [string]$PIMEligible.PIMStartDateTime + PIMEligibilityEndDateTime = [string]$PIMEligible.PIMEndDateTime + }) + + if (-not $NoAADGroupsResolveMembers) { + if ($PIMEligible.IdentityType -eq 'Group') { + if ($htAADGroupsDetails.($PIMEligible.IdentityObjectId)) { + foreach ($groupMemberUser in $htAADGroupsDetails.($PIMEligible.IdentityObjectId).MembersUsers) { + #$tfCountCnt++ + $null = $PIMEligibleEnriched.Add([PSCustomObject]@{ + Scope = $PIMEligible.ScopeType + ScopeId = $PIMEligible.ScopeId + ScopeName = $PIMEligible.ScopeDisplayName + ManagementGroupId = $PIMEligible.ManagementGroupId + ManagementGroupDisplayName = $PIMEligible.ManagementGroupDisplayName + SubscriptionId = $PIMEligible.SubscriptionId + SubscriptionDisplayName = $PIMEligible.SubscriptionDisplayName + MgPath = $PIMEligible.MgPath -join '/' + MgLevel = $PIMEligible.MgLevel + Role = $roleName + RoleClear = $PIMEligible.RoleName + RoleId = $PIMEligible.RoleId + RoleIdGuid = $PIMEligible.RoleIdGuid + RoleType = $PIMEligible.RoleType + IdentityObjectId = $groupMemberUser.id + IdentityDisplayName = $groupMemberUser.displayName + IdentitySignInName = $groupMemberUser.userPrincipalName + IdentityType = "User $($groupMemberUser.userType)" + IdentityApplicability = 'nested' + AppliesThrough = "$($PIMEligible.IdentityDisplayName) ($($PIMEligible.IdentityObjectId))" + PIMEligibilityId = $PIMEligible.PIMId + PIMEligibility = $PIMEligible.PIMInheritance + PIMEligibilityInheritedFrom = $PIMEligible.PIMInheritedFrom + PIMEligibilityInheritedFromClear = $PIMEligible.PIMInheritedFromClear + PIMEligibilityStartDateTime = [string]$PIMEligible.PIMStartDateTime + PIMEligibilityEndDateTime = [string]$PIMEligible.PIMEndDateTime + }) + } + } + else { + Write-Host "!! Unexpected: Group $($PIMEligible.IdentityDisplayName) ($($PIMEligible.IdentityObjectId)) not found in `$htAADGroupsDetails - please report back!" + } + } + } + } + $endPIMEnrichment = Get-Date + Write-Host " PIMEnrichment duration: $((New-TimeSpan -Start $startPIMEnrichment -End $endPIMEnrichment).TotalMinutes) minutes ($((New-TimeSpan -Start $startPIMEnrichment -End $endPIMEnrichment).TotalSeconds) seconds)" + + if (-not $NoPIMEligibilityIntegrationRoleAssignmentsAll) { + $startPIMEnrichmentToRBACAll = Get-Date + Write-Host ' Processing PIMEnrichment to RBACAll' + foreach ($PIMEligibleRoleAssignment in $PIMEligibleEnriched) { + if ($PIMEligibleRoleAssignment.PIMEligibility -eq 'Inherited') { + $scope = "inherited $($PIMEligibleRoleAssignment.PIMEligibilityInheritedFromClear)" + } + else { + $scope = "thisScope $($PIMEligibleRoleAssignment.Scope)" + } + + if (-not [string]::IsNullOrEmpty($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleDataActions) -or -not [string]::IsNullOrEmpty($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleNotDataActions)) { + $roleManageData = 'true' + } + else { + $roleManageData = 'false' + } + + $roleCanDoRoleAssignments = $false + if ($htCacheDefinitionsRole.($PIMEligibleRoleAssignment.RoleId).RoleCanDoRoleAssignments) { + $roleCanDoRoleAssignments = 'true' + } + + $null = $script:rbacAll.Add([PSCustomObject]@{ + Level = $PIMEligibleRoleAssignment.MgLevel + RoleAssignmentId = '' + RoleAssignmentPIMRelated = $true + RoleAssignmentPIMAssignmentType = 'Eligible' + RoleAssignmentPIMAssignmentSlotStart = $PIMEligibleRoleAssignment.PIMEligibilityStartDateTime + RoleAssignmentPIMAssignmentSlotEnd = $PIMEligibleRoleAssignment.PIMEligibilityEndDateTime + CreatedBy = '' + CreatedOn = '' + UpdatedBy = $rbac.RoleAssignmentUpdatedBy + UpdatedOn = $rbac.RoleAssignmentUpdatedOn + MgId = $PIMEligibleRoleAssignment.ManagementGroupId + MgName = $PIMEligibleRoleAssignment.ManagementGroupDisplayName + MgParentId = '' #check + MgParentName = '' #check + SubscriptionId = $PIMEligibleRoleAssignment.SubscriptionId + SubscriptionName = $PIMEligibleRoleAssignment.SubscriptionDisplayName + Scope = $scope + ScopeTenOrMgOrSubOrRGOrRes = $PIMEligibleRoleAssignment.Scope + RoleAssignmentScopeName = $PIMEligibleRoleAssignment.Scope + RoleAssignmentScopeRG = '' + RoleAssignmentScopeRes = '' + Role = $PIMEligibleRoleAssignment.Role + RoleClear = $PIMEligibleRoleAssignment.RoleClear + RoleId = $PIMEligibleRoleAssignment.RoleIdGuid + RoleType = $PIMEligibleRoleAssignment.RoleType + RoleDataRelated = $roleManageData #check + AssignmentType = $PIMEligibleRoleAssignment.IdentityApplicability + AssignmentInheritFrom = $PIMEligibleRoleAssignment.AppliesThrough + GroupMembersCount = '' + ObjectDisplayName = $PIMEligibleRoleAssignment.IdentityDisplayName + ObjectSignInName = $PIMEligibleRoleAssignment.IdentitySignInName + ObjectId = $PIMEligibleRoleAssignment.IdentityObjectId + ObjectType = $PIMEligibleRoleAssignment.IdentityType + RbacRelatedPolicyAssignment = '' + RbacRelatedPolicyAssignmentClear = '' + RoleSecurityCustomRoleOwner = '' #check $rbac.RoleSecurityCustomRoleOwner + RoleSecurityOwnerAssignmentSP = '' #check $rbac.RoleSecurityOwnerAssignmentSP + RoleCanDoRoleAssignments = $roleCanDoRoleAssignments + }) + } + $endPIMEnrichmentToRBACAll = Get-Date + Write-Host " PIMEnrichment to RBACAll duration: $((New-TimeSpan -Start $startPIMEnrichmentToRBACAll -End $endPIMEnrichmentToRBACAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startPIMEnrichmentToRBACAll -End $endPIMEnrichmentToRBACAll).TotalSeconds) seconds)" + } + } + #endregion PIMEligible + + Write-Host ' Processing unresoved Identities (createdBy)' + $startUnResolvedIdentitiesCreatedBy = Get-Date + #prep prepUnresoledIdentities + #region identitiesThatCreatedRoleAssignmentsButDontHaveARoleAssignmentThemselve + $script:htIdentitiesWithRoleAssignmentsUnique = @{} + $identitiesWithRoleAssignmentsUnique = $rbacAll.where( { $_.ObjectType -ne 'Unknown' } ) | Sort-Object -Property ObjectId -Unique | Select-Object ObjectType, ObjectDisplayName, ObjectSignInName, ObjectId + foreach ($identityWithRoleAssignment in $identitiesWithRoleAssignmentsUnique | Sort-Object -Property objectType) { + + if (-not $htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId)) { + $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId) = @{} + + $arr = @() + $ht = [ordered]@{} + $identityWithRoleAssignment.psobject.properties | ForEach-Object { + if ($_.Value) { + $value = $_.Value + } + else { + $value = 'n/a' + } + $arr += "$($_.Name): $value" + $ht.($_.Name) = $value + } + + $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId).details = $arr -join "$CsvDelimiterOpposite " + $script:htIdentitiesWithRoleAssignmentsUnique.($identityWithRoleAssignment.ObjectId).detailsJson = $ht + } + } + #endregion identitiesThatCreatedRoleAssignmentsButDontHaveARoleAssignmentThemselve + + #enrich rbacAll with createdBy and UpdatedBy identity information + #region enrichrbacAll + $htNonResolvedIdentities = @{} + foreach ($rbac in $rbacAll) { + $createdBy = $rbac.createdBy + if (-not [string]::IsNullOrEmpty($createdBy)) { + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + $rbac.CreatedBy = $createdBy + } + else { + if (-not $htNonResolvedIdentities.($rbac.createdBy)) { + $htNonResolvedIdentities.($rbac.createdBy) = @{} + } + } + } + + $updatedBy = $rbac.updatedBy + if (-not [string]::IsNullOrEmpty($updatedBy)) { + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { + $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details + $rbac.UpdatedBy = $updatedBy + } + else { + if (-not $htNonResolvedIdentities.($rbac.updatedBy)) { + $htNonResolvedIdentities.($rbac.updatedBy) = @{} + } + } + } + } + #endregion enrichrbacAll + + #region nonResolvedIdentities + $htNonResolvedIdentitiesCount = $htNonResolvedIdentities.Count + if ($htNonResolvedIdentitiesCount -gt 0) { + Write-Host " $htNonResolvedIdentitiesCount unresolved identities that created a RBAC Role assignment (createdBy)" + $arrayUnresolvedIdentities = @() + $arrayUnresolvedIdentities = foreach ($unresolvedIdentity in $htNonResolvedIdentities.keys) { + if (-not [string]::IsNullOrEmpty($unresolvedIdentity)) { + $unresolvedIdentity + } + } + $arrayUnresolvedIdentitiesCount = $arrayUnresolvedIdentities.Count + Write-Host " $arrayUnresolvedIdentitiesCount unresolved identities that have a value" + if ($arrayUnresolvedIdentitiesCount -gt 0) { + + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 1000 + $ObjectBatch = $arrayUnresolvedIdentities | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + $ObjectBatchCount = ($ObjectBatch | Measure-Object).Count + $batchCnt = 0 + + $script:htResolvedIdentities = @{} + + foreach ($batch in $ObjectBatch) { + $batchCnt++ + + $nonResolvedIdentitiesToCheck = '"{0}"' -f ($batch.Group.where({ testGuid $_ }) -join '","') + Write-Host " IdentitiesToCheck: Batch #$batchCnt/$($ObjectBatchCount) ($(($batch.Group).Count))" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/directoryObjects/getByIds" + $method = 'POST' + $body = @" + { + "ids":[$($nonResolvedIdentitiesToCheck)] + } +"@ + + function resolveIdentitiesRBAC($currentTask) { + $resolvedIdentities = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask + $resolvedIdentitiesCount = $resolvedIdentities.Count + Write-Host " $resolvedIdentitiesCount identities resolved" + if ($resolvedIdentitiesCount -gt 0) { + + foreach ($resolvedIdentity in $resolvedIdentities) { + + if (-not $htResolvedIdentities.($resolvedIdentity.id)) { + + $script:htResolvedIdentities.($resolvedIdentity.id) = @{} + if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.servicePrincipal' -or $resolvedIdentity.'@odata.type' -eq '#microsoft.graph.user') { + if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + if ($resolvedIdentity.servicePrincipalType -eq 'ManagedIdentity') { + $miType = 'unknown' + foreach ($altName in $resolvedIdentity.alternativeNames) { + if ($altName -like 'isExplicit=*') { + $splitAltName = $altName.split('=') + if ($splitAltName[1] -eq 'true') { + $miType = 'Usr' + } + if ($splitAltName[1] -eq 'false') { + $miType = 'Sys' + } + } + } + $sptype = "MI $miType" + $custObjectType = "ObjectType: SP $sptype, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" + $ht = @{} + $ht.'ObjectType' = "SP $sptype" + $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) + $ht.'ObjectSignInName' = 'n/a' + $ht.'ObjectId' = $resolvedIdentity.id + } + else { + if ($resolvedIdentity.servicePrincipalType -eq 'Application') { + $sptype = 'App' + if ($resolvedIdentity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { + $custObjectType = "ObjectType: SP $sptype INT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" + $ht = @{} + $ht.'ObjectType' = "SP $sptype INT" + $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) + $ht.'ObjectSignInName' = 'n/a' + $ht.'ObjectId' = $resolvedIdentity.id + } + else { + $custObjectType = "ObjectType: SP $sptype EXT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (r)" + $ht = @{} + $ht.'ObjectType' = "SP $sptype EXT" + $ht.'ObjectDisplayName' = $($resolvedIdentity.displayName) + $ht.'ObjectSignInName' = 'n/a' + $ht.'ObjectId' = $resolvedIdentity.id + } + } + else { + Write-Host "* * * Unexpected IdentityType $($resolvedIdentity.servicePrincipalType)" + } + } + $script:htResolvedIdentities.($resolvedIdentity.id).custObjectType = $custObjectType + $script:htResolvedIdentities.($resolvedIdentity.id).obj = $resolvedIdentity + } + + if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.user') { + if ($htParamteters.DoNotShowRoleAssignmentsUserData) { + $hlpObjectDisplayName = 'scrubbed' + $hlpObjectSigninName = 'scrubbed' + } + else { + $hlpObjectDisplayName = $resolvedIdentity.displayName + $hlpObjectSigninName = $resolvedIdentity.userPrincipalName + } + $custObjectType = "ObjectType: User, ObjectDisplayName: $hlpObjectDisplayName, ObjectSignInName: $hlpObjectSigninName, ObjectId: $($resolvedIdentity.id) (r)" + $ht = @{} + $ht.'ObjectType' = 'User' + $ht.'ObjectDisplayName' = $hlpObjectDisplayName + $ht.'ObjectSignInName' = $hlpObjectSigninName + $ht.'ObjectId' = $resolvedIdentity.id + + $script:htResolvedIdentities.($resolvedIdentity.id).custObjectType = $custObjectType + $script:htResolvedIdentities.($resolvedIdentity.id).obj = $resolvedIdentity + } + if (-not $htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id)) { + $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id) = @{} + $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id).details = $custObjectType + $script:htIdentitiesWithRoleAssignmentsUnique.($resolvedIdentity.id).detailsJson = $ht + } + } + + if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { + Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow + } + } + } + } + } + resolveIdentitiesRBAC -currentTask ' resolveObjectbyId RoleAssignment' + } + + foreach ($rbac in $rbacAll.where( { $_.CreatedBy -notlike 'ObjectType*' -or $_.UpdatedBy -notlike 'ObjectType*' })) { + if ($rbac.CreatedBy -notlike 'ObjectType*') { + if ($htResolvedIdentities.($rbac.CreatedBy)) { + $rbac.CreatedBy = $htResolvedIdentities.($rbac.CreatedBy).custObjectType + } + else { + if ($rbac.RoleAssignmentPIMAssignmentType -eq 'Eligible') { + $rbac.CreatedBy = '' + } + else { + if ([string]::IsNullOrEmpty($rbac.CreatedBy)) { + $rbac.CreatedBy = 'IsNullOrEmpty' + } + else { + $rbac.CreatedBy = "$($rbac.CreatedBy)" + } + } + } + } + if ($rbac.UpdatedBy -notlike 'ObjectType*') { + if ($htResolvedIdentities.($rbac.UpdatedBy)) { + $rbac.UpdatedBy = $htResolvedIdentities.($rbac.UpdatedBy).custObjectType + } + else { + if ($rbac.RoleAssignmentPIMAssignmentType -eq 'Eligible') { + $rbac.UpdatedBy = '' + } + else { + if ([string]::IsNullOrEmpty($rbac.UpdatedBy)) { + $rbac.UpdatedBy = 'IsNullOrEmpty' + } + else { + $rbac.UpdatedBy = "$($rbac.UpdatedBy)" + } + } + } + } + } + } + } + $endUnResolvedIdentitiesCreatedBy = Get-Date + Write-Host " UnresolvedIdentities (createdBy) duration: $((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedBy -End $endUnResolvedIdentitiesCreatedBy).TotalMinutes) minutes ($((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedBy -End $endUnResolvedIdentitiesCreatedBy).TotalSeconds) seconds)" + #endregion nonResolvedIdentities + + $startRBACAllGrouping = Get-Date + $script:rbacAllGroupedBySubscription = $rbacAll | Group-Object -Property SubscriptionId + $script:rbacAllGroupedByManagementGroup = $rbacAll | Group-Object -Property MgId + $endRBACAllGrouping = Get-Date + Write-Host " RBACAll Grouping duration: $((New-TimeSpan -Start $startRBACAllGrouping -End $endRBACAllGrouping).TotalMinutes) minutes ($((New-TimeSpan -Start $startRBACAllGrouping -End $endRBACAllGrouping).TotalSeconds) seconds)" + $endCreateRBACAll = Get-Date + Write-Host " CreateRBACAll duration: $((New-TimeSpan -Start $startCreateRBACAll -End $endCreateRBACAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startCreateRBACAll -End $endCreateRBACAll).TotalSeconds) seconds)" + #endregion tenantSummaryPre + + showMemoryUsage + + #region tenantSummaryPolicy + $htmlTenantSummary = [System.Text.StringBuilder]::new() + [void]$htmlTenantSummary.AppendLine(@' + +
+ Anything which can help you learn Azure Policy GitHub
+'@) + + #region SUMMARYcustompolicies + $startCustPolLoop = Get-Date + Write-Host ' processing TenantSummary Custom Policy definitions' + + $script:customPoliciesDetailed = [System.Collections.ArrayList]@() + $script:tenantPoliciesDetailed = [System.Collections.ArrayList]@() + foreach ($tenantPolicy in (($htCacheDefinitionsPolicy).Values | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + + #uniqueAssignments + $policyUniqueAssignments = $null + if ($htPolicyWithAssignmentsBase.($tenantPolicy.PolicyDefinitionId)) { + $policyUniqueAssignments = $htPolicyWithAssignmentsBase.($tenantPolicy.PolicyDefinitionId).Assignments | Sort-Object + $policyUniqueAssignmentsCount = ($policyUniqueAssignments).count + } + else { + $policyUniqueAssignmentsCount = 0 + } + + $uniqueAssignments = $null + if ($policyUniqueAssignmentsCount -gt 0) { + $policyUniqueAssignmentsList = "($($policyUniqueAssignments -join "$CsvDelimiterOpposite "))" + $uniqueAssignments = "$policyUniqueAssignmentsCount $policyUniqueAssignmentsList" + } + else { + $uniqueAssignments = $policyUniqueAssignmentsCount + } + + #PolicyUsedInPolicySet + $usedInPolicySet4JSON = $null + $usedInPolicySet = 0 + $usedInPolicySet4CSV = '' + $usedInPolicySetCount = 0 + if (($htPoliciesUsedInPolicySets).($tenantPolicy.PolicyDefinitionId)) { + $hlpPolicySetUsed = ($htPoliciesUsedInPolicySets).($tenantPolicy.PolicyDefinitionId) + $usedInPolicySet4JSON = $hlpPolicySetUsed.PolicySetIdOnly | Sort-Object + $usedInPolicySet = "$(($hlpPolicySetUsed.PolicySet | Sort-Object) -join "$CsvDelimiterOpposite ")" + $usedInPolicySet4CSV = "$(($hlpPolicySetUsed.PolicySet4CSV | Sort-Object) -join "$CsvDelimiterOpposite ")" + $usedInPolicySetCount = ($hlpPolicySetUsed.PolicySet).Count + } + + #policyEffect + if ($tenantPolicy.effectDefaultValue -ne 'n/a') { + $effect = "Default: $($tenantPolicy.effectDefaultValue); Allowed: $($tenantPolicy.effectAllowedValue)" + } + elseif ($tenantPolicy.effectFixedValue -ne 'n/a') { + $effect = "Fixed: $($tenantPolicy.effectFixedValue)" + } + else { + $effect = 'n/a' + } + + if (($tenantPolicy.RoleDefinitionIds) -ne 'n/a') { + $policyRoleDefinitionsArray = @() + $policyRoleDefinitionsArray = foreach ($roleDefinitionId in $tenantPolicy.RoleDefinitionIds | Sort-Object) { + if (($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').LinkToAzAdvertizer) { + ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').LinkToAzAdvertizer + } + else { + ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').Name -replace '<', '<' -replace '>', '>' + } + } + $policyRoleDefinitionsClearArray = @() + $policyRoleDefinitionsClearArray = foreach ($roleDefinitionId in $tenantPolicy.RoleDefinitionIds | Sort-Object) { + ($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').Name + } + $policyRoleDefinitions = $policyRoleDefinitionsArray -join "$CsvDelimiterOpposite " + $policyRoleDefinitionsClear = $policyRoleDefinitionsClearArray -join "$CsvDelimiterOpposite " + } + else { + $policyRoleDefinitions = 'n/a' + $policyRoleDefinitionsClear = 'n/a' + } + + # if ($tenantPolicy.Json.properties.metadata.version) { + # $policyVersion = $tenantPolicy.Json.properties.metadata.version + # } + # else { + # $policyVersion = 'n/a' + # } + + if ($tenantPolicy.Type -eq 'Custom') { + + $createdOn = '' + $createdBy = '' + $createdByJson = '' + $updatedOn = '' + $updatedBy = '' + $updatedByJson = '' + if ($tenantPolicy.Json.properties.metadata.createdOn) { + $createdOn = $tenantPolicy.Json.properties.metadata.createdOn + } + if ($tenantPolicy.Json.properties.metadata.createdBy) { + $createdBy = $tenantPolicy.Json.properties.metadata.createdBy + $createdByJson = $createdBy + if ($createdBy -ne 'n/a') { + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdByJson = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).detailsJson + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + + } + } + } + if ($tenantPolicy.Json.properties.metadata.updatedOn) { + $updatedOn = $tenantPolicy.Json.properties.metadata.updatedOn + } + if ($tenantPolicy.Json.properties.metadata.updatedBy) { + $updatedBy = $tenantPolicy.Json.properties.metadata.updatedBy + $updatedByJson = $updatedBy + if ($updatedBy -ne 'n/a') { + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { + $updatedByJson = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).detailsJson + $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details + + } + } + } + + $null = $script:customPoliciesDetailed.Add([PSCustomObject]@{ + Type = 'Custom' + ScopeMGLevel = $tenantPolicy.ScopeMGLevel + Scope = $tenantPolicy.ScopeMgSub + ScopeId = $tenantPolicy.ScopeId + PolicyDisplayName = $tenantPolicy.DisplayName + PolicyDefinitionName = $tenantPolicy.Name + PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId + PolicyVersion = $tenantPolicy.Version + PolicyEffect = $effect + PolicyCategory = $tenantPolicy.Category + RoleDefinitions = $policyRoleDefinitions + RoleDefinitionsClear = $policyRoleDefinitionsClear + UniqueAssignments = $uniqueAssignments + UsedInPolicySetsCount = $usedInPolicySetCount + UsedInPolicySets = $usedInPolicySet + UsedInPolicySet4CSV = $usedInPolicySet4CSV + CreatedOn = $createdOn + CreatedBy = $createdBy + UpdatedOn = $updatedOn + UpdatedBy = $updatedBy + ALZ = $tenantPolicy.ALZ + ALZState = $tenantPolicy.ALZState + ALZLatestVer = $tenantPolicy.ALZLatestVer + ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel + ALZPolicyName = $tenantPolicy.ALZPolicyName + #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + }) + + $null = $script:tenantPoliciesDetailed.Add([PSCustomObject]@{ + Type = 'Custom' + ScopeMGLevel = $tenantPolicy.ScopeMGLevel + Scope = $tenantPolicy.ScopeMgSub + ScopeId = $tenantPolicy.ScopeId + PolicyDisplayName = $tenantPolicy.DisplayName + PolicyDefinitionName = $tenantPolicy.Name + PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId + PolicyVersion = $tenantPolicy.Version + PolicyEffect = $effect + PolicyCategory = $tenantPolicy.Category + UniqueAssignmentsCount = $policyUniqueAssignmentsCount + UniqueAssignments = $policyUniqueAssignments + UsedInPolicySetsCount = $usedInPolicySetCount + UsedInPolicySets = $usedInPolicySet + UsedInPolicySet4CSV = $usedInPolicySet4CSV + UsedInPolicySet4JSON = $usedInPolicySet4JSON + CreatedOn = $createdOn + CreatedBy = $createdBy + CreatedByJson = $createdByJson + UpdatedOn = $updatedOn + UpdatedBy = $updatedBy + UpdatedByJson = $updatedByJson + #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + Json = $tenantPolicy.Json + ALZ = $tenantPolicy.ALZ + ALZState = $tenantPolicy.ALZState + ALZLatestVer = $tenantPolicy.ALZLatestVer + ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel + ALZPolicyName = $tenantPolicy.ALZPolicyName + }) + } + else { + $null = $script:tenantPoliciesDetailed.Add([PSCustomObject]@{ + Type = $tenantPolicy.Type + ScopeMGLevel = $null + Scope = $null + ScopeId = $null + PolicyDisplayName = $tenantPolicy.DisplayName + PolicyDefinitionName = $tenantPolicy.Name + PolicyDefinitionId = $tenantPolicy.PolicyDefinitionId + PolicyVersion = $tenantPolicy.Version + PolicyEffect = $effect + PolicyCategory = $tenantPolicy.Category + UniqueAssignmentsCount = $policyUniqueAssignmentsCount + UniqueAssignments = $policyUniqueAssignments + UsedInPolicySetsCount = $usedInPolicySetCount + UsedInPolicySets = $usedInPolicySet + UsedInPolicySet4CSV = $usedInPolicySet4CSV + UsedInPolicySet4JSON = $usedInPolicySet4JSON + CreatedOn = $null + CreatedBy = $null + CreatedByJson = $null + UpdatedOn = $null + UpdatedBy = $null + UpdatedByJson = $null + #Json = [string]($tenantPolicy.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + Json = $tenantPolicy.Json + ALZ = $tenantPolicy.ALZ + ALZState = $tenantPolicy.ALZState + ALZLatestVer = $tenantPolicy.ALZLatestVer + ALZIdentificationLevel = $tenantPolicy.ALZIdentificationLevel + ALZPolicyName = $tenantPolicy.ALZPolicyName + }) + } + } + + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_PolicyDefinitions" + Write-Host " Exporting PolicyDefinitions CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $tenantPoliciesDetailed | Sort-Object -Property Type, Scope, PolicyDefinitionId | Select-Object -ExcludeProperty UniqueAssignments, UsedInPolicySets, UsedInPolicySet4JSON, CreatedByJson, UpdatedByJson, Json | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + + if ($getMgParentName -eq 'Tenant Root') { + + if ($tenantCustomPoliciesCount -gt 0) { + $tfCount = $tenantCustomPoliciesCount + $htmlTableId = 'TenantSummary_customPolicies' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYcustompolicies = $null + $htmlSUMMARYcustompolicies = foreach ($customPolicy in ($customPoliciesDetailed | Sort-Object @{Expression = { $_.PolicyDisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + if ($custompolicy.UsedInPolicySetsCount -gt 0) { + $customPolicyUsedInPolicySets = "$($customPolicy.UsedInPolicySetsCount) ($($customPolicy.UsedInPolicySets))" + } + else { + $customPolicyUsedInPolicySets = $($customPolicy.UsedInPolicySetsCount) + } + @" + + + + + + + + + + + + + + + + + +"@ + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYcustompolicies) + $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlTenantSummary = [System.Text.StringBuilder]::new() + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScope IdPolicy DisplayNamePolicy NamePolicyIdCategoryALZEffectRole definitionsUnique assignmentsUsed in PolicySetsCreatedOnCreatedByUpdatedOnUpdatedBy
$($customPolicy.Scope)$($customPolicy.ScopeId)$($customPolicy.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyCategory -replace '<', '<' -replace '>', '>')$($customPolicy.ALZ)$($customPolicy.PolicyEffect)$($customPolicy.RoleDefinitions)$($customPolicy.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)$($customPolicy.CreatedOn)$($customPolicy.CreatedBy)$($customPolicy.UpdatedOn)$($customPolicy.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$tenantCustomPoliciesCount Custom Policy definitions ($scopeNamingSummary)

+"@) + } + } + #SUMMARY NOT tenant total custom policy definitions + else { + $faimage = "" + + if ($tenantCustomPoliciesCount -gt 0) { + $tfCount = $tenantCustomPoliciesCount + $customPoliciesInScopeArray = [System.Collections.ArrayList]@() + foreach ($customPolicy in ($tenantCustomPolicies | Sort-Object @{Expression = { $_.DisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + if (($customPolicy.PolicyDefinitionId) -like '/providers/Microsoft.Management/managementGroups/*') { + $policyScopedMgSub = $customPolicy.PolicyDefinitionId -replace '/providers/Microsoft.Management/managementGroups/', '' -replace '/.*' + if ($mgsAndSubs.MgId -contains ($policyScopedMgSub)) { + $null = $customPoliciesInScopeArray.Add($customPolicy) + } + } + + if (($customPolicy.PolicyDefinitionId) -like '/subscriptions/*') { + $policyScopedMgSub = $customPolicy.PolicyDefinitionId -replace '/subscriptions/', '' -replace '/.*' + if ($mgsAndSubs.SubscriptionId -contains ($policyScopedMgSub)) { + $null = $customPoliciesInScopeArray.Add($customPolicy) + } + else { + #Write-Host "$policyScopedMgSub NOT in Scope" + } + } + } + $customPoliciesFromSuperiorMGs = $tenantCustomPoliciesCount - (($customPoliciesInScopeArray).count) + } + else { + $customPoliciesFromSuperiorMGs = '0' + } + + if ($tenantCustomPoliciesCount -gt 0) { + $tfCount = $tenantCustomPoliciesCount + $htmlTableId = 'TenantSummary_customPolicies' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYcustompolicies = $null + $htmlSUMMARYcustompolicies = foreach ($customPolicy in ($customPoliciesDetailed | Sort-Object @{Expression = { $_.PolicyDisplayName } }, @{Expression = { $_.PolicyDefinitionId } })) { + if ($custompolicy.UsedInPolicySetsCount -gt 0) { + $customPolicyUsedInPolicySets = "$($customPolicy.UsedInPolicySetsCount) ($($customPolicy.UsedInPolicySets))" + } + else { + $customPolicyUsedInPolicySets = $($customPolicy.UsedInPolicySetsCount) + } + @" + + + + + + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYcustompolicies) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScope IdPolicy DisplayNamePolicy NamePolicyIdCategoryALZPolicy EffectRole definitionsUnique assignmentsUsed in PolicySetsCreatedOnCreatedByUpdatedOnUpdatedBy
$($customPolicy.Scope)$($customPolicy.ScopeId)$($customPolicy.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionName -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicy.PolicyCategory -replace '<', '<' -replace '>', '>')$($customPolicy.ALZ)$($customPolicy.PolicyEffect)$($customPolicy.RoleDefinitions)$($customPolicy.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)$($customPolicy.CreatedOn)$($customPolicy.CreatedBy)$($customPolicy.UpdatedOn)$($customPolicy.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$tenantCustomPoliciesCount Custom Policy definitions ($scopeNamingSummary)

+"@) + } + } + $endCustPolLoop = Get-Date + Write-Host " Custom Policy processing duration: $((New-TimeSpan -Start $startCustPolLoop -End $endCustPolLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startCustPolLoop -End $endCustPolLoop).TotalSeconds) seconds)" + #endregion SUMMARYcustompolicies + + $startcustpolorph = Get-Date + #region SUMMARYCustomPoliciesOrphandedTenantRoot + Write-Host ' processing TenantSummary Custom Policy definitions orphaned' + if ($getMgParentName -eq 'Tenant Root') { + $customPoliciesOrphaned = [System.Collections.ArrayList]@() + foreach ($customPolicyAll in $tenantCustomPolicies) { + if (($policyPolicyBaseQueryUniqueCustomDefinitions).count -eq 0) { + $null = $customPoliciesOrphaned.Add($customPolicyAll) + } + else { + if ($policyPolicyBaseQueryUniqueCustomDefinitions -notcontains ($customPolicyAll.PolicyDefinitionId)) { + $null = $customPoliciesOrphaned.Add($customPolicyAll) + } + } + } + + $arrayCustomPoliciesOrphanedFinal = [System.Collections.ArrayList]@() + foreach ($customPolicyOrphaned in $customPoliciesOrphaned) { + if ($customPolicyOrphaned.Id) { + if (-not $htPoliciesUsedInPolicySets.($customPolicyOrphaned.Id)) { + $null = $arrayCustomPoliciesOrphanedFinal.Add($customPolicyOrphaned) + } + } + else { + Write-Host '!!!!!!!!!!!!!!!!!!!!! no Id' + Write-Host '## all:' + $customPoliciesOrphaned + Write-Host '## customPolicyOrphaned no Id:' + $customPolicyOrphaned + } + } + + #rgchange + $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + foreach ($customPolicyOrphanedFinal in $arrayCustomPoliciesOrphanedFinal) { + if (($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values.properties.PolicyDefinitionId -notcontains $customPolicyOrphanedFinal.PolicyDefinitionId) { + $null = $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups.Add($customPolicyOrphanedFinal) + } + } + + if (($arrayCustomPoliciesOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arrayCustomPoliciesOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customPoliciesOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + $htmlSUMMARYCustomPoliciesOrphandedTenantRoot = $null + $htmlSUMMARYCustomPoliciesOrphandedTenantRoot = foreach ($customPolicyOrphaned in $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.PolicyDefinitionId } }, @{Expression = { $_.DisplayName } }) { + @" + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYCustomPoliciesOrphandedTenantRoot) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy DisplayNamePolicyId
$($customPolicyOrphaned.DisplayName)$($customPolicyOrphaned.PolicyDefinitionId)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($customPoliciesOrphaned).count) Orphaned Custom Policy definitions ($scopeNamingSummary)

+"@) + } + } + #SUMMARY Custom Policy definitions Orphanded NOT TenantRoot + else { + $customPoliciesOrphaned = [System.Collections.ArrayList]@() + foreach ($customPolicyAll in $tenantCustomPolicies) { + if (($policyPolicyBaseQueryUniqueCustomDefinitions).count -eq 0) { + $null = $customPoliciesOrphaned.Add($customPolicyAll) + } + else { + if ($policyPolicyBaseQueryUniqueCustomDefinitions -notcontains ($customPolicyAll.PolicyDefinitionId)) { + $null = $customPoliciesOrphaned.Add($customPolicyAll) + } + } + } + + $customPoliciesOrphanedInScopeArray = [System.Collections.ArrayList]@() + foreach ($customPolicyOrphaned in $customPoliciesOrphaned) { + $hlpOrphanedInScope = $customPolicyOrphaned + if (($hlpOrphanedInScope.PolicyDefinitionId) -like '/providers/Microsoft.Management/managementGroups/*') { + $policyScopedMgSub = $hlpOrphanedInScope.PolicyDefinitionId -replace '/providers/Microsoft.Management/managementGroups/' -replace '/.*' + if ($mgsAndSubs.MgId -contains ($policyScopedMgSub)) { + $null = $customPoliciesOrphanedInScopeArray.Add($hlpOrphanedInScope) + } + } + if (($hlpOrphanedInScope.PolicyDefinitionId) -like '/subscriptions/*') { + $policyScopedMgSub = $hlpOrphanedInScope.PolicyDefinitionId -replace '/subscriptions/' -replace '/.*' + if ($mgsAndSubs.SubscriptionId -contains ($policyScopedMgSub)) { + $null = $customPoliciesOrphanedInScopeArray.Add($hlpOrphanedInScope) + } + } + } + + $arrayCustomPoliciesOrphanedFinal = [System.Collections.ArrayList]@() + foreach ($customPolicyOrphanedInScopeArray in $customPoliciesOrphanedInScopeArray) { + if (-not $htPoliciesUsedInPolicySets.($customPolicyOrphanedInScopeArray.Id)) { + $null = $arrayCustomPoliciesOrphanedFinal.Add($customPolicyOrphanedInScopeArray) + } + } + + $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + foreach ($customPolicyOrphanedFinal in $arrayCustomPoliciesOrphanedFinal) { + if (($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values.properties.PolicyDefinitionId -notcontains $customPolicyOrphanedFinal.PolicyDefinitionId) { + $null = $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups.Add($customPolicyOrphanedFinal) + } + } + + if (($arrayCustomPoliciesOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arrayCustomPoliciesOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customPoliciesOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + $htmlSUMMARYCustomPoliciesOrphandedTenantRoot = $null + $htmlSUMMARYCustomPoliciesOrphandedTenantRoot = foreach ($customPolicyOrphaned in $arrayCustomPoliciesOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.PolicyDefinitionId } }, @{Expression = { $_.DisplayName } }) { + @" + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYCustomPoliciesOrphandedTenantRoot) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy DisplayNamePolicyId
$($customPolicyOrphaned.DisplayName)$($customPolicyOrphaned.PolicyDefinitionId)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($arrayCustomPoliciesOrphanedFinalIncludingResourceGroups.count) Orphaned Custom Policy definitions ($scopeNamingSummary)

+"@) + } + } + #endregion SUMMARYCustomPoliciesOrphandedTenantRoot + $endcustpolorph = Get-Date + Write-Host " processing TenantSummary Custom Policy definitions orphaned duration: $((New-TimeSpan -Start $startcustpolorph -End $endcustpolorph).TotalSeconds) seconds" + + #region SUMMARYtenanttotalcustompolicySets + $startCustPolSetLoop = Get-Date + Write-Host ' processing TenantSummary Custom PolicySet definitions' + $script:customPolicySetsDetailed = [System.Collections.ArrayList]@() + $script:tenantPolicySetsDetailed = [System.Collections.ArrayList]@() + $custompolicySetsInScopeArray = [System.Collections.ArrayList]@() + foreach ($tenantPolicySet in ($tenantAllPolicySets)) { + + $policySetUniqueAssignments = $policyPolicySetBaseQueryUniqueAssignments.where( { $_.PolicyDefinitionId -eq $tenantPolicySet.Id }).PolicyAssignmentId + $policySetUniqueAssignmentsArray = [System.Collections.ArrayList]@() + foreach ($policySetUniqueAssignment in $policySetUniqueAssignments) { + $null = $policySetUniqueAssignmentsArray.Add($policySetUniqueAssignment) + } + $policySetUniqueAssignmentsCount = ($policySetUniqueAssignments).count + if ($policySetUniqueAssignmentsCount -gt 0) { + $policySetUniqueAssignmentsList = "($($policySetUniqueAssignmentsArray -join "$CsvDelimiterOpposite "))" + $policySetUniqueAssignment = "$policySetUniqueAssignmentsCount $policySetUniqueAssignmentsList" + } + else { + $policySetUniqueAssignment = $policySetUniqueAssignmentsCount + } + + $policySetPoliciesArray = [System.Collections.ArrayList]@() + $policySetPoliciesArrayClean = [System.Collections.ArrayList]@() + $policySetPoliciesArrayIdOnly = [System.Collections.ArrayList]@() + $policySetPoliciesBuiltinArrayIdOnlyCSV = [System.Collections.ArrayList]@() + $policySetPoliciesStaticArrayIdOnlyCSV = [System.Collections.ArrayList]@() + $policySetPoliciesCustomArrayIdOnlyCSV = [System.Collections.ArrayList]@() + foreach ($policyPolicySet in $tenantPolicySet.PolicySetPolicyIds) { + $hlpPolicyDef = ($htCacheDefinitionsPolicy).($policyPolicySet) + + if ($hlpPolicyDef.Type -eq 'Builtin' -or $hlpPolicyDef.Type -eq 'Static') { + $null = $policySetPoliciesArray.Add("$($hlpPolicyDef.LinkToAzAdvertizer) ($policyPolicySet)") + if ($hlpPolicyDef.Type -eq 'Builtin') { + $null = $policySetPoliciesBuiltinArrayIdOnlyCSV.Add($policyPolicySet -replace '/providers/microsoft.authorization/policydefinitions/') + } + if ($hlpPolicyDef.Type -eq 'Static') { + $null = $policySetPoliciesStaticArrayIdOnlyCSV.Add($policyPolicySet -replace '/providers/microsoft.authorization/policydefinitions/') + } + } + else { + $null = $policySetPoliciesCustomArrayIdOnlyCSV.Add($policyPolicySet) + if ($hlpPolicyDef.DisplayName) { + if ([string]::IsNullOrEmpty($hlpPolicyDef.DisplayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = $hlpPolicyDef.DisplayName + } + } + else { + $displayName = 'noDisplayNameGiven' + } + $null = $policySetPoliciesArray.Add("$($displayName -replace '<', '<' -replace '>', '>') ($policyPolicySet)") + } + + if ($hlpPolicyDef.DisplayName) { + if ([string]::IsNullOrEmpty($hlpPolicyDef.DisplayName)) { + $displayName = 'noDisplayNameGiven' + } + else { + $displayName = $hlpPolicyDef.DisplayName + } + } + else { + $displayName = 'noDisplayNameGiven' + } + + $null = $policySetPoliciesArrayClean.Add("$($displayName) ($policyPolicySet)") + $null = $policySetPoliciesArrayIdOnly.Add($policyPolicySet) + } + + if ($policySetPoliciesArrayIdOnly.Count -eq 0) { + $policySetPoliciesArrayIdOnly = $null + } + + if ($policySetPoliciesBuiltinArrayIdOnlyCSV.Count -eq 0) { + $policySetPoliciesBuiltinArrayIdOnlyCSV = $null + } + if ($policySetPoliciesStaticArrayIdOnlyCSV.Count -eq 0) { + $policySetPoliciesStaticArrayIdOnlyCSV = $null + } + if ($policySetPoliciesCustomArrayIdOnlyCSV.Count -eq 0) { + $policySetPoliciesCustomArrayIdOnlyCSV = $null + } + + $policySetPoliciesCount = ($policySetPoliciesArray).count + if ($policySetPoliciesCount -gt 0) { + $policiesUsed = "$policySetPoliciesCount ($(($policySetPoliciesArray | Sort-Object) -join "$CsvDelimiterOpposite "))" + $policiesUsedClean = "$policySetPoliciesCount ($(($policySetPoliciesArrayClean | Sort-Object) -join "$CsvDelimiterOpposite "))" + } + else { + $policiesUsed = '0 really?' + $policiesUsedClean = '0 really?' + } + + if ($tenantPolicySet.Json.properties.metadata.version) { + $policySetVersion = $tenantPolicySet.Json.properties.metadata.version + } + else { + $policySetVersion = 'n/a' + } + + if ($tenantPolicySet.Type -eq 'Custom') { + #inscopeOrNot + if ($getMgParentName -ne 'Tenant Root') { + if ($mgsAndSubs.MgId -contains ($tenantPolicySet.ScopeId)) { + $null = $custompolicySetsInScopeArray.Add($tenantPolicySet) + } + if ($mgsAndSubs.SubscriptionId -contains ($tenantPolicySet.ScopeId)) { + $null = $custompolicySetsInScopeArray.Add($tenantPolicySet) + } + } + + $createdOn = '' + $createdBy = '' + $createdByJson = '' + $updatedOn = '' + $updatedBy = '' + $updatedByJson = '' + if ($tenantPolicySet.Json.properties.metadata.createdOn) { + $createdOn = $tenantPolicySet.Json.properties.metadata.createdOn + } + if ($tenantPolicySet.Json.properties.metadata.createdBy) { + $createdBy = $tenantPolicySet.Json.properties.metadata.createdBy + $createdByJson = $createdBy + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdByJson = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).detailsJson + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + } + } + if ($tenantPolicySet.Json.properties.metadata.updatedOn) { + $updatedOn = $tenantPolicySet.Json.properties.metadata.updatedOn + } + if ($tenantPolicySet.Json.properties.metadata.updatedBy) { + $updatedBy = $tenantPolicySet.Json.properties.metadata.updatedBy + $updatedByJson = $updatedBy + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { + $updatedByJson = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).detailsJson + $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details + + } + } + + $null = $script:customPolicySetsDetailed.Add([PSCustomObject]@{ + Type = 'Custom' + ScopeMGLevel = $tenantPolicySet.ScopeMGLevel + Scope = $tenantPolicySet.ScopeMgSub + ScopeId = $tenantPolicySet.ScopeId + PolicySetDisplayName = $tenantPolicySet.DisplayName + PolicySetDefinitionName = $tenantPolicySet.Name + PolicySetDefinitionId = $tenantPolicySet.PolicyDefinitionId + PolicySetCategory = $tenantPolicySet.Category + UniqueAssignments = $policySetUniqueAssignment + PoliciesUsed = $policiesUsed + PoliciesUsedClean = $policiesUsedClean + CreatedOn = $createdOn + CreatedBy = $createdBy + UpdatedOn = $updatedOn + UpdatedBy = $updatedBy + #Json = [string]($tenantPolicySet.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + ALZ = $tenantPolicySet.ALZ + ALZState = $tenantPolicySet.ALZState + ALZLatestVer = $tenantPolicySet.ALZLatestVer + ALZIdentificationLevel = $tenantPolicySet.ALZIdentificationLevel + ALZPolicySetName = $tenantPolicySet.ALZPolicySetName + }) + + $null = $script:tenantPolicySetsDetailed.Add([PSCustomObject]@{ + Type = 'Custom' + ScopeMGLevel = $tenantPolicySet.ScopeMGLevel + Scope = $tenantPolicySet.ScopeMgSub + ScopeId = $tenantPolicySet.ScopeId + PolicySetDisplayName = $tenantPolicySet.DisplayName + PolicySetDescription = $tenantPolicySet.Description + PolicySetDefinitionName = $tenantPolicySet.Name + PolicySetDefinitionId = $tenantPolicySet.PolicyDefinitionId + PolicySetCategory = $tenantPolicySet.Category + PolicySetVersion = $tenantPolicySet.Version + UniqueAssignmentsCount = $policySetUniqueAssignmentsCount + UniqueAssignments = $policySetUniqueAssignments + PoliciesUsedCount = $policySetPoliciesCount + PoliciesUsedBuiltinCount = $policySetPoliciesBuiltinArrayIdOnlyCSV.Count + PoliciesUsedStaticCount = $policySetPoliciesStaticArrayIdOnlyCSV.Count + PoliciesUsedCustomCount = $policySetPoliciesCustomArrayIdOnlyCSV.Count + PoliciesUsed = $policySetPoliciesArrayClean + PoliciesUsed4JSON = $policySetPoliciesArrayIdOnly + PoliciesUsedBuiltin = $policySetPoliciesBuiltinArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + PoliciesUsedStatic = $policySetPoliciesStaticArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + PoliciesUsedCustom = $policySetPoliciesCustomArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + CreatedOn = $createdOn + CreatedBy = $createdBy + CreatedByJson = $createdByJson + UpdatedOn = $updatedOn + UpdatedBy = $updatedBy + UpdatedByJson = $updatedByJson + #Json = [string]($tenantPolicySet.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + Json = $tenantPolicySet.Json + ALZ = $tenantPolicySet.ALZ + ALZState = $tenantPolicySet.ALZState + ALZLatestVer = $tenantPolicySet.ALZLatestVer + ALZIdentificationLevel = $tenantPolicySet.ALZIdentificationLevel + ALZPolicySetName = $tenantPolicySet.ALZPolicySetName + }) + + } + else { + $null = $script:tenantPolicySetsDetailed.Add([PSCustomObject]@{ + Type = 'BuiltIn' + ScopeMGLevel = $null + Scope = $null + ScopeId = $null + PolicySetDisplayName = $tenantPolicySet.DisplayName + PolicySetDescription = $tenantPolicySet.Description + PolicySetDefinitionName = $tenantPolicySet.Name + PolicySetDefinitionId = $tenantPolicySet.PolicyDefinitionId + PolicySetCategory = $tenantPolicySet.Category + PolicySetVersion = $tenantPolicySet.Version + UniqueAssignmentsCount = $policySetUniqueAssignmentsCount + UniqueAssignments = $policySetUniqueAssignments + PoliciesUsedCount = $policySetPoliciesCount + PoliciesUsedBuiltinCount = $policySetPoliciesBuiltinArrayIdOnlyCSV.Count + PoliciesUsedStaticCount = $policySetPoliciesStaticArrayIdOnlyCSV.Count + PoliciesUsedCustomCount = $policySetPoliciesCustomArrayIdOnlyCSV.Count + PoliciesUsed = $policySetPoliciesArrayClean + PoliciesUsed4JSON = $policySetPoliciesArrayIdOnly + PoliciesUsedBuiltin = $policySetPoliciesBuiltinArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + PoliciesUsedStatic = $policySetPoliciesStaticArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + PoliciesUsedCustom = $policySetPoliciesCustomArrayIdOnlyCSV -join "$CsvDelimiterOpposite " + CreatedOn = '' + CreatedBy = '' + CreatedByJson = $null + UpdatedOn = '' + UpdatedBy = '' + UpdatedByJson = $null + #Json = [string]($tenantPolicySet.Json | ConvertTo-Json -Depth 99 -EnumsAsStrings) + Json = $tenantPolicySet.Json + ALZ = $tenantPolicySet.ALZ + ALZState = $tenantPolicySet.ALZState + ALZLatestVer = $tenantPolicySet.ALZLatestVer + ALZIdentificationLevel = $tenantPolicySet.ALZIdentificationLevel + ALZPolicySetName = $tenantPolicySet.ALZPolicySetName + }) + } + } + + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_PolicySetDefinitions" + Write-Host " Exporting PolicySetDefinitions CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $tenantPolicySetsDetailed | Select-Object -ExcludeProperty UniqueAssignments, PoliciesUsed, PoliciesUsed4JSON, CreatedByJson, UpdatedByJson, Json | Sort-Object -Property Type, Scope, PolicySetDefinitionId | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + + if ($getMgParentName -eq 'Tenant Root') { + if ($tenantCustompolicySetsCount -gt $LimitPOLICYPolicySetDefinitionsScopedTenant * ($LimitCriticalPercentage / 100)) { + $faimage = "" + } + else { + $faimage = "" + } + + if ($tenantCustompolicySetsCount -gt 0) { + $tfCount = $tenantCustompolicySetsCount + $htmlTableId = 'TenantSummary_customPolicySets' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYtenanttotalcustompolicySets = $null + $htmlSUMMARYtenanttotalcustompolicySets = foreach ($customPolicySet in $customPolicySetsDetailed | Sort-Object @{Expression = { $_.Scope } }, @{Expression = { $_.PolicySetDisplayName } }, @{Expression = { $_.PolicySetDefinitionId } }) { + @" + + + + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYtenanttotalcustompolicySets) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScopeIdPolicySet DisplayNamePolicySet NamePolicySetIdCategoryALZUnique assignmentsPolicies used in PolicySetCreatedOnCreatedByUpdatedOnUpdatedBy
$($customPolicySet.Scope)$($customPolicySet.ScopeId)$($customPolicySet.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetDefinitionName -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetCategory -replace '<', '<' -replace '>', '>')$($customPolicySet.ALZ)$($customPolicySet.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicySet.PoliciesUsed)$($customPolicySet.CreatedOn)$($customPolicySet.CreatedBy)$($customPolicySet.UpdatedOn)$($customPolicySet.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$tenantCustomPolicySetsCount Custom PolicySet definitions ($scopeNamingSummary)

+"@) + } + } + #SUMMARY NOT tenant total custom policySet definitions + else { + $faimage = "" + if ($tenantCustompolicySetsCount -gt $LimitPOLICYPolicySetDefinitionsScopedTenant * ($LimitCriticalPercentage / 100)) { + $faimage = "" + } + else { + $faimage = "" + } + + if ($tenantCustompolicySetsCount -gt 0) { + $custompolicySetsFromSuperiorMGs = $tenantCustompolicySetsCount - (($custompolicySetsInScopeArray).count) + } + else { + $custompolicySetsFromSuperiorMGs = '0' + } + + if ($tenantCustompolicySetsCount -gt 0) { + $tfCount = $tenantCustompolicySetsCount + $htmlTableId = 'TenantSummary_customPolicySets' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYtenanttotalcustompolicySets = $null + $htmlSUMMARYtenanttotalcustompolicySets = foreach ($customPolicySet in $customPolicySetsDetailed | Sort-Object @{Expression = { $_.Scope } }, @{Expression = { $_.PolicySetDisplayName } }, @{Expression = { $_.PolicySetDefinitionId } }) { + @" + + + + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYtenanttotalcustompolicySets) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScope IdPolicySet DisplayNamePolicySet NamePolicySetIdCategoryALZUnique assignmentsPolicies used in PolicySetCreatedOnCreatedByUpdatedOnUpdatedBy
$($customPolicySet.Scope)$($customPolicySet.ScopeId)$($customPolicySet.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetDefinitionName -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($customPolicySet.PolicySetCategory -replace '<', '<' -replace '>', '>')$($customPolicySet.ALZ)$($customPolicySet.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicySet.PoliciesUsed)$($customPolicySet.CreatedOn)$($customPolicySet.CreatedBy)$($customPolicySet.UpdatedOn)$($customPolicySet.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$tenantCustomPolicySetsCount Custom PolicySet definitions ($scopeNamingSummary)

+"@) + } + } + $endCustPolSetLoop = Get-Date + Write-Host " Custom PolicySet processing duration: $((New-TimeSpan -Start $startCustPolSetLoop -End $endCustPolSetLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startCustPolSetLoop -End $endCustPolSetLoop).TotalSeconds) seconds)" + #endregion SUMMARYtenanttotalcustompolicySets + + #region SUMMARYCustompolicySetOrphandedTenantRoot + Write-Host ' processing TenantSummary Custom PolicySet definitions orphaned' + if ($getMgParentName -eq 'Tenant Root') { + $custompolicySetSetsOrphaned = [System.Collections.ArrayList]@() + foreach ($custompolicySetAll in $tenantCustomPolicySets) { + if (($policyPolicySetBaseQueryUniqueCustomDefinitions).count -eq 0) { + $null = $custompolicySetSetsOrphaned.Add($custompolicySetAll) + } + else { + if ($policyPolicySetBaseQueryUniqueCustomDefinitions -notcontains ($custompolicySetAll.Id)) { + $null = $custompolicySetSetsOrphaned.Add($custompolicySetAll) + } + } + } + + $arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + foreach ($customPolicySetOrphaned in $custompolicySetSetsOrphaned) { + if (($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values.properties.PolicyDefinitionId -notcontains $customPolicySetOrphaned.PolicyDefinitionId) { + $null = $arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups.Add($customPolicySetOrphaned) + } + } + + if (($arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customPolicySetsOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + $htmlSUMMARYCustompolicySetOrphandedTenantRoot = $null + $htmlSUMMARYCustompolicySetOrphandedTenantRoot = foreach ($custompolicySetOrphaned in $arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.PolicyDefinitionId } }, @{Expression = { $_.DisplayName } }) { + @" + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYCustompolicySetOrphandedTenantRoot) + [void]$htmlTenantSummary.AppendLine(@" + +
PolicySet DisplayNamePolicySetId
$($custompolicySetOrphaned.DisplayName)$($custompolicySetOrphaned.PolicyDefinitionId)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($arraycustompolicySetSetsOrphanedFinalIncludingResourceGroups).count) Orphaned Custom PolicySet definitions ($scopeNamingSummary)

+"@) + } + } + #SUMMARY Custom policySetSets Orphanded NOT TenantRoot + else { + $arraycustompolicySetsOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + foreach ($custompolicySetAll in $tenantCustomPolicySets) { + $isOrphaned = 'unknown' + if (($policyPolicySetBaseQueryUniqueCustomDefinitions).count -eq 0) { + $isOrphaned = 'potentially' + } + else { + if ($policyPolicySetBaseQueryUniqueCustomDefinitions -notcontains $custompolicySetAll.Id) { + $isOrphaned = 'potentially' + } + } + + if ($isOrphaned -eq 'potentially') { + $isInScope = 'unknown' + if ($custompolicySetAll.PolicyDefinitionId -like '/providers/Microsoft.Management/managementGroups/*') { + $policySetScopedMgSub = $custompolicySetAll.PolicyDefinitionId -replace '/providers/Microsoft.Management/managementGroups/', '' -replace '/.*' + if ($mgsAndSubs.MgId -contains ($policySetScopedMgSub)) { + $isInScope = 'inScope' + } + } + elseif ($custompolicySetAll.PolicyDefinitionId -like '/subscriptions/*') { + $policySetScopedMgSub = $custompolicySetAll.PolicyDefinitionId -replace '/subscriptions/', '' -replace '/.*' + if ($mgsAndSubs.SubscriptionId -contains ($policySetScopedMgSub)) { + $isInScope = 'inScope' + } + } + else { + Write-Host 'unexpected' + } + + if ($isInScope -eq 'inScope') { + if (($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values.properties.PolicyDefinitionId -notcontains $custompolicySetAll.PolicyDefinitionId) { + $null = $arraycustompolicySetsOrphanedFinalIncludingResourceGroups.Add($custompolicySetAll) + } + } + } + } + + if (($arraycustompolicySetsOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arraycustompolicySetsOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customPolicySetsOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + $htmlSUMMARYCustompolicySetOrphandedTenantRoot = $null + $htmlSUMMARYCustompolicySetOrphandedTenantRoot = foreach ($custompolicySetOrphaned in $arraycustompolicySetsOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.PolicyDefinitionId } }, @{Expression = { $_.DisplayName } }) { + @" + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYCustompolicySetOrphandedTenantRoot) + [void]$htmlTenantSummary.AppendLine(@" + +
PolicySet DisplayNamePolicySetId
$($custompolicySetOrphaned.DisplayName)$($custompolicySetOrphaned.policyDefinitionId)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($arraycustompolicySetsOrphanedFinalIncludingResourceGroups).count) Orphaned Custom PolicySet definitions ($scopeNamingSummary)

+"@) + } + } + #endregion SUMMARYCustompolicySetOrphandedTenantRoot + + #region SUMMARYPolicyParityCustomBuiltIn + Write-Host ' processing TenantSummary Policy parity custom built-in' + + if ($arrayCustomBuiltInPolicyParity.Count -gt 0) { + $tfCount = $arrayCustomBuiltInPolicyParity.Count + $htmlTableId = 'TenantSummary_PolicyCustomBuiltInParity' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + +"@) + + $htmlSUMMARYPolicyCustomBuiltInParity = $null + $htmlSUMMARYPolicyCustomBuiltInParity = foreach ($entry in $arrayCustomBuiltInPolicyParity | Sort-Object -Property CustomPolicyId) { + $arrayBuiltinsRef = @() + foreach ($builtInPolicyId in $entry.BuiltInPolicyId) { + $arrayBuiltinsRef += "$($htCacheDefinitionsPolicy.($builtInPolicyId).DisplayName) ($($builtInPolicyId -replace '.*/'))" + } + $builtInPolicyAzA = $arrayBuiltinsRef -join ', ' + @" + + + + + + + + +"@ + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyCustomBuiltInParity) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy NamePolicy DisplayNamePolicy CategoryPolicy Id# match built-inBuilt-In Policy
$($entry.CustomPolicyName)$($entry.CustomPolicyDisplayName)$($entry.CustomPolicyCategory)$($entry.CustomPolicyId)$($entry.MatchBuiltinPolicyCount)$($builtInPolicyAzA)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No custom Policy definition(s) built-in Policy rule parity

+'@) + } + #endregion SUMMARYPolicyParityCustomBuiltIn + + #region SUMMARYALZPolicies + Write-Host ' processing TenantSummary ALZPolicies' + + if (-not $NoALZPolicyVersionChecker) { + + $alzPoliciesInTenant = [System.Collections.ArrayList]@() + #policies + foreach ($policy in ($htCacheDefinitionsPolicy).Values.where({ $_.ALZ -eq $true })) { + if ($policy.ALZState -ne 'obsolete' -and $policy.ALZState -ne 'unknown') { + $ALZVersion = $alzPolicies.($policy.ALZPolicyName).latestVersion + $azAdvertizerUrl = "https://www.azadvertizer.net/azpolicyadvertizer/$($policy.ALZPolicyName).html" + } + else { + $ALZVersion = '' + $azAdvertizerUrl = '' + } + $null = $alzPoliciesInTenant.Add([PSCustomObject]@{ + Type = 'Policy' + PolicyName = $policy.Name + PolicyId = $policy.PolicyDefinitionId + PolicyVersion = $policy.Version + PolicyScope = $policy.ScopeMgSub + PolicyScopeId = $policy.ScopeId + ALZPolicyName = $policy.ALZPolicyName + ALZVersion = $ALZVersion + ALZState = $policy.ALZState + InTenant = $true + DetectedBy = $policy.ALZIdentificationLevel + AzAdvertizerUrl = $azAdvertizerUrl + }) + } + foreach ($alzPolicy in $alzPolicies.keys) { + if ($alzPolicies.($alzPolicy).status -eq 'Prod') { + if ($alzPoliciesInTenant.PolicyName -notcontains $alzPolicy) { + $null = $alzPoliciesInTenant.Add([PSCustomObject]@{ + Type = 'Policy' + PolicyName = 'n/a' + PolicyId = 'n/a' + PolicyVersion = 'n/a' + PolicyScope = 'n/a' + PolicyScopeId = 'n/a' + ALZPolicyName = $alzPolicy + ALZVersion = $alzPolicies.($alzPolicy).latestVersion + ALZState = '' + InTenant = $false + DetectedBy = 'ALZ GitHub repository' + AzAdvertizerUrl = "https://www.azadvertizer.net/azpolicyadvertizer/$($alzPolicy).html" + }) + } + } + } + + #policysets + foreach ($policySet in ($htCacheDefinitionsPolicySet).Values.where({ $_.ALZ -eq $true })) { + + if ($policySet.ALZState -ne 'obsolete' -and $policySet.ALZState -ne 'unknown') { + $ALZVersion = $alzPolicySets.($policySet.ALZPolicySetName).latestVersion + $azAdvertizerUrl = "https://www.azadvertizer.net/azpolicyinitiativesadvertizer/$($policySet.ALZPolicySetName).html" + } + else { + $ALZVersion = '' + $azAdvertizerUrl = '' + } + $null = $alzPoliciesInTenant.Add([PSCustomObject]@{ + Type = 'PolicySet' + PolicyName = $policySet.Name + PolicyId = $policySet.PolicyDefinitionId + PolicyVersion = $policySet.Version + PolicyScope = $policySet.ScopeMgSub + PolicyScopeId = $policySet.ScopeId + ALZPolicyName = $policySet.ALZPolicySetName + ALZVersion = $ALZVersion + ALZState = $policySet.ALZState + InTenant = $true + DetectedBy = $policySet.ALZIdentificationLevel + AzAdvertizerUrl = $azAdvertizerUrl + }) + } + + foreach ($alzPolicySet in $alzPolicySets.keys) { + if ($alzPolicySets.($alzPolicySet).status -eq 'Prod') { + if ($alzPoliciesInTenant.PolicyName -notcontains $alzPolicySet) { + $null = $alzPoliciesInTenant.Add([PSCustomObject]@{ + Type = 'PolicySet' + PolicyName = 'n/a' + PolicyId = 'n/a' + PolicyVersion = 'n/a' + PolicyScope = 'n/a' + PolicyScopeId = 'n/a' + ALZPolicyName = $alzPolicySet + ALZVersion = $alzPolicySets.($alzPolicySet).latestVersion + ALZState = '' + InTenant = $false + DetectedBy = 'ALZ GitHub repository' + AzAdvertizerUrl = "https://www.azadvertizer.net/azpolicyinitiativesadvertizer/$($alzPolicySet).html" + }) + } + } + } + + if ($alzPoliciesInTenant.Count -gt 0) { + $tfCount = $alzPoliciesInTenant.Count + $htmlTableId = 'TenantSummary_ALZPolicies' + $abbrALZ = " " + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Landing Zones (ALZ) GitHub
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + +"@) + + $htmlSUMMARYALZPolicyVersionChecker = $null + $exemptionData4CSVExport = [System.Collections.ArrayList]@() + $alzPoliciesInTenantSorted = $alzPoliciesInTenant | Sort-Object -Property PolicyName, PolicyId, ALZPolicyName, Type + $htmlSUMMARYALZPolicyVersionChecker = foreach ($entry in $alzPoliciesInTenantSorted) { + if ([string]::IsNullOrWhiteSpace($entry.AzAdvertizerUrl)) { + $link = '' + } + else { + $link = "AzA Link " + } + @" + + + + + + + + + + + + + +"@ + } + + if (-not $NoCsvExport) { + Write-Host "Exporting 'Azure Landing Zones (ALZ) Policy Version Checker' CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv'" + $alzPoliciesInTenantSorted | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ALZPolicyVersionChecker.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYALZPolicyVersionChecker) + [void]$htmlTenantSummary.AppendLine(@" + +
TypePolicy Name (Id)Policy VersionPolicy ScopePolicy Scope IdALZ Policy Name (Id)ALZ Policy VersionALZ State$($abbrALZ)Exists in tenantDetection methodAzAdvertizer Link
$($entry.Type)$($entry.PolicyName)$($entry.PolicyVersion)$($entry.PolicyScope)$($entry.PolicyScopeId)$($entry.ALZPolicyName)$($entry.ALZVersion)$($entry.ALZState)$($entry.InTenant)$($entry.DetectedBy)$link
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

Azure Landing Zones (ALZ) Policy Version Checker

+'@) + } + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Azure Landing Zones (ALZ) Policy Version Checker (parameter -NoALZPolicyVersionChecker = $NoALZPolicyVersionChecker)

+"@) + } + #endregion SUMMARYALZPolicies + + $startcustpolsetdeprpol = Get-Date + #region SUMMARYPolicySetsDeprecatedPolicy + Write-Host ' processing TenantSummary Custom PolicySet definitions using deprected Policy' + $policySetsDeprecated = [System.Collections.ArrayList]@() + $customPolicySetsCount = ($tenantCustomPolicySets).count + if ($customPolicySetsCount -gt 0) { + foreach ($polSetDef in $tenantCustomPolicySets) { + foreach ($polsetPolDefId in $polSetDef.PolicySetPolicyIds) { + $hlpDeprecatedPolicySet = (($htCacheDefinitionsPolicy).($polsetPolDefId)) + if ($hlpDeprecatedPolicySet.Type -eq 'BuiltIn') { + if ($hlpDeprecatedPolicySet.Deprecated -eq $true -or ($hlpDeprecatedPolicySet.DisplayName).StartsWith('[Deprecated]', 'CurrentCultureIgnoreCase')) { + $null = $policySetsDeprecated.Add([PSCustomObject]@{ + PolicySetDisplayName = $polSetDef.DisplayName + PolicySetDefinitionId = $polSetDef.PolicyDefinitionId + PolicyDisplayName = $hlpDeprecatedPolicySet.DisplayName + PolicyId = $hlpDeprecatedPolicySet.Id + DeprecatedProperty = $hlpDeprecatedPolicySet.Deprecated + }) + } + } + } + } + } + + if (($policySetsDeprecated).count -gt 0) { + $tfCount = ($policySetsDeprecated).count + $htmlTableId = 'TenantSummary_policySetsDeprecated' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYPolicySetsDeprecatedPolicy = $null + $htmlSUMMARYPolicySetsDeprecatedPolicy = foreach ($policySetDeprecated in $policySetsDeprecated | Sort-Object @{Expression = { $_.PolicySetDisplayName } }, @{Expression = { $_.PolicySetDefinitionId } }) { + + if ($policySetDeprecated.DeprecatedProperty -eq $true) { + $deprecatedProperty = 'true' + } + else { + $deprecatedProperty = 'false' + } + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicySetsDeprecatedPolicy) + [void]$htmlTenantSummary.AppendLine(@" + +
PolicySet DisplayNamePolicySetIdPolicy DisplayNamePolicyIdDeprecated Property
$($policySetDeprecated.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($policySetDeprecated.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($policySetDeprecated.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($policySetDeprecated.PolicyId -replace '<', '<' -replace '>', '>')$deprecatedProperty
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($policySetsDeprecated).count) PolicySets / deprecated built-in Policy

+"@) + } + #endregion SUMMARYPolicySetsDeprecatedPolicy + $endcustpolsetdeprpol = Get-Date + Write-Host " processing PolicySetsDeprecatedPolicy duration: $((New-TimeSpan -Start $startcustpolsetdeprpol -End $endcustpolsetdeprpol).TotalSeconds) seconds" + + $startcustpolassdeprpol = Get-Date + #region SUMMARYPolicyAssignmentsDeprecatedPolicy + Write-Host ' processing TenantSummary PolicyAssignments using deprecated Policy' + $policyAssignmentsDeprecated = [System.Collections.ArrayList]@() + foreach ($policyAssignmentAll in ($htCacheAssignmentsPolicy).Values) { + + $hlpAssignmentDeprecatedPolicy = $policyAssignmentAll.Assignment + $hlpPolicyDefinitionId = ($hlpAssignmentDeprecatedPolicy.properties.policyDefinitionId).ToLower() + #policySet + if ($($htCacheDefinitionsPolicySet).(($hlpPolicyDefinitionId))) { + foreach ($polsetPolDefId in $($htCacheDefinitionsPolicySet).(($hlpPolicyDefinitionId)).PolicySetPolicyIds) { + $hlpDeprecatedAssignment = (($htCacheDefinitionsPolicy).(($polsetPolDefId))) + if ($hlpDeprecatedAssignment.type -eq 'BuiltIn') { + if ($hlpDeprecatedAssignment.Deprecated -eq $true) { + $null = $policyAssignmentsDeprecated.Add([PSCustomObject]@{ + PolicyAssignmentDisplayName = $hlpAssignmentDeprecatedPolicy.properties.displayName + PolicyAssignmentId = ($hlpAssignmentDeprecatedPolicy.id).Tolower() + PolicyDisplayName = $hlpDeprecatedAssignment.DisplayName + PolicyId = $hlpDeprecatedAssignment.Id + PolicySetDisplayName = ($htCacheDefinitionsPolicySet).(($hlpPolicyDefinitionId)).DisplayName + PolicySetId = ($htCacheDefinitionsPolicySet).(($hlpPolicyDefinitionId)).PolicyDefinitionId + PolicyType = 'PolicySet' + DeprecatedProperty = $hlpDeprecatedAssignment.Deprecated + }) + } + } + } + } + + #Policy + $hlpDeprecatedAssignmentPol = ($htCacheDefinitionsPolicy).(($hlpPolicyDefinitionId)) + if ($hlpDeprecatedAssignmentPol) { + if ($hlpDeprecatedAssignmentPol.type -eq 'BuiltIn') { + if ($hlpDeprecatedAssignmentPol.Deprecated -eq $true) { + $null = $policyAssignmentsDeprecated.Add([PSCustomObject]@{ + PolicyAssignmentDisplayName = $hlpAssignmentDeprecatedPolicy.properties.displayName + PolicyAssignmentId = ($hlpAssignmentDeprecatedPolicy.id).Tolower() + PolicyDisplayName = $hlpDeprecatedAssignmentPol.DisplayName + PolicyId = $hlpDeprecatedAssignmentPol.Id + PolicyType = 'Policy' + DeprecatedProperty = $hlpDeprecatedAssignmentPol.Deprecated + PolicySetDisplayName = 'n/a' + PolicySetId = 'n/a' + }) + } + } + } + } + + + if (($policyAssignmentsDeprecated).count -gt 0) { + $tfCount = ($policyAssignmentsDeprecated).count + $htmlTableId = 'TenantSummary_policyAssignmentsDeprecated' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYPolicyAssignmentsDeprecatedPolicy = $null + $htmlSUMMARYPolicyAssignmentsDeprecatedPolicy = foreach ($policyAssignmentDeprecated in $policyAssignmentsDeprecated | Sort-Object @{Expression = { $_.PolicyAssignmentDisplayName } }, @{Expression = { $_.PolicyAssignmentId } }) { + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyAssignmentsDeprecatedPolicy) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy Assignment DisplayNamePolicy AssignmentIdPolicy/PolicySetPolicySet DisplayNamePolicySetIdPolicy DisplayNamePolicyIdDeprecated Property
$($policyAssignmentDeprecated.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.PolicyType)$($policyAssignmentDeprecated.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.PolicySetId -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.PolicyId -replace '<', '<' -replace '>', '>')$($policyAssignmentDeprecated.DeprecatedProperty)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($policyAssignmentsDeprecated).count) Policy assignments / deprecated built-in Policy

+"@) + } + #endregion SUMMARYPolicyAssignmentsDeprecatedPolicy + $endcustpolassdeprpol = Get-Date + Write-Host " processing PolicyAssignmentsDeprecatedPolicy duration: $((New-TimeSpan -Start $startcustpolassdeprpol -End $endcustpolassdeprpol).TotalSeconds) seconds" + + #region SUMMARYPolicyExemptions + Write-Host ' processing TenantSummary Policy exemptions' + $policyExemptionsCount = ($htPolicyAssignmentExemptions.Keys).Count + + if ($policyExemptionsCount -gt 0) { + $tfCount = $policyExemptionsCount + $htmlTableId = 'TenantSummary_policyExemptions' + + $expiredExemptionsCount = ($htPolicyAssignmentExemptions.Keys | Where-Object { $htPolicyAssignmentExemptions.($_).exemption.properties.expiresOn -and $htPolicyAssignmentExemptions.($_).exemption.properties.expiresOn -lt (Get-Date).ToUniversalTime() } | Measure-Object).count + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + $htmlSUMMARYPolicyExemptions = $null + $exemptionData4CSVExport = [System.Collections.ArrayList]@() + $htmlSUMMARYPolicyExemptions = foreach ($policyExemption in $htPolicyAssignmentExemptions.Keys | Sort-Object) { + $exemption = $htPolicyAssignmentExemptions.$policyExemption.exemption + if ($exemption.properties.expiresOn) { + $exemptionExpiresOnFormated = (($exemption.properties.expiresOn)) + if ($exemption.properties.expiresOn -gt (Get-Date).ToUniversalTime()) { + $exemptionExpiresOn = $exemptionExpiresOnFormated + } + else { + $exemptionExpiresOn = "expired $($exemptionExpiresOnFormated)" + } + } + else { + $exemptionExpiresOn = 'n/a' + } + + $splitExemptionId = ($exemption.Id).Split('/') + if (($exemption.Id) -like '/subscriptions/*') { + + switch (($splitExemptionId).Count - 1) { + #sub + 6 { + $exemptionScope = 'Sub' + $subId = $splitExemptionId[2] + $subdetails = $htSubDetails.($subId).details + $mgId = $subdetails.MgId + $mgName = $subdetails.MgName + $subName = $subdetails.Subscription + $rgName = '' + $resName = '' + } + + #rg + 8 { + $exemptionScope = 'RG' + $subId = $splitExemptionId[2] + $subdetails = $htSubDetails.($subId).details + $mgId = $subdetails.MgId + $mgName = $subdetails.MgName + $subName = $subdetails.Subscription + $rgName = $splitExemptionId[4] + $resName = '' + } + + #res + 12 { + $exemptionScope = 'Res' + $subId = $splitExemptionId[2] + $subdetails = $htSubDetails.($subId).details + $mgId = $subdetails.MgId + $mgName = $subdetails.MgName + $subName = $subdetails.Subscription + $rgName = $splitExemptionId[4] + $resName = "$($splitExemptionId[8]) / $($splitExemptionId[6..7] -join '/')" + } + } + } + else { + $exemptionScope = 'MG' + $mgId = $splitExemptionId[4] + $mgdetails = $htMgDetails.($mgId).details + $mgName = $mgdetails.MgName + $subId = '' + $subName = '' + $rgName = '' + $resName = '' + } + + $policyType = 'unknown' + $policy = 'unknown' + $arrayExemptedPolicies = @() + $arrayExemptedPoliciesCSV = @() + $policiesExempted = $null + $policiesExemptedCSV = $null + $policiesExemptedCSVCount = $null + $policiesTotalCount = $null + if ($htCacheAssignmentsPolicy.(($exemption.properties.policyAssignmentId).tolower()).Assignment.properties.policyDefinitionId) { + $policyDefinitionId = $htCacheAssignmentsPolicy.(($exemption.properties.policyAssignmentId).tolower()).Assignment.properties.policyDefinitionId + + if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { + $policyType = 'Policy' + if ($htCacheDefinitionsPolicy.($policyDefinitionId.tolower())) { + $policyDetail = $htCacheDefinitionsPolicy.($policyDefinitionId.tolower()) + if ($policyDetail.Type -eq 'BuiltIn') { + $policy = $policyDetail.LinkToAzAdvertizer + } + else { + $policy = "$($policyDetail.DisplayName) ($($policyDetail.Id))" + } + $policiesExempted = $null + $policyClear = "$($policyDetail.DisplayName) ($($policyDetail.Id))" + } + } + + if ($policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { + $policyType = 'PolicySet' + if ($htCacheDefinitionsPolicySet.($policyDefinitionId.tolower())) { + $policyDetail = $htCacheDefinitionsPolicySet.($policyDefinitionId.tolower()) + if ($policyDetail.Type -eq 'BuiltIn') { + $policy = $policyDetail.LinkToAzAdvertizer + } + else { + $policy = "$($policyDetail.DisplayName) ($($policyDetail.Id))" + } + $policiesTotalCount = $htCacheDefinitionsPolicySet.($policyDefinitionId.tolower()).PolicySetPolicyRefIds.Count + if ($exemption.properties.policyDefinitionReferenceIds.Count -gt 0) { + foreach ($exemptedRefId in $exemption.properties.policyDefinitionReferenceIds) { + $policyExempted = 'unknown' + $policyExemptedCSV = 'unknown' + if ($htCacheDefinitionsPolicySet.($policyDefinitionId.tolower()).PolicySetPolicyRefIds.($exemptedRefId)) { + $exemptedPolicyId = $htCacheDefinitionsPolicySet.($policyDefinitionId.tolower()).PolicySetPolicyRefIds.($exemptedRefId) + if ($htCacheDefinitionsPolicy.($exemptedPolicyId.tolower())) { + $policyExemptedDetail = $htCacheDefinitionsPolicy.($exemptedPolicyId.tolower()) + if ($policyExemptedDetail.Type -eq 'BuiltIn') { + $policyExempted = $policyExemptedDetail.LinkToAzAdvertizer + } + else { + $policyExempted = "$($policyExemptedDetail.DisplayName) ($($policyExemptedDetail.Id))" + } + $policyExemptedCSV = "$($policyExemptedDetail.DisplayName) ($($policyExemptedDetail.Id))" + + } + } + $arrayExemptedPolicies += $policyExempted + $arrayExemptedPoliciesCSV += $policyExemptedCSV + } + + $policiesExempted = "$($arrayExemptedPolicies.Count)/$($policiesTotalCount) (
$(($arrayExemptedPolicies | Sort-Object) -join '
'))" + $policiesExemptedCSV = ($arrayExemptedPoliciesCSV | Sort-Object) -join "$CsvDelimiterOpposite " + $policiesExemptedCSVCount = $arrayExemptedPoliciesCSV.Count + } + else { + $policiesExempted = "all $policiesTotalCount" + $policiesExemptedCSV = "all $policiesTotalCount" + $policiesExemptedCSVCount = $policiesTotalCount + } + + $policyClear = "$($policyDetail.DisplayName) ($($policyDetail.Id))" + } + } + + } + + if (-not $NoCsvExport) { + $null = $exemptionData4CSVExport.Add([PSCustomObject]@{ + Scope = $exemptionScope + ManagementGroupId = $mgId + ManagementGroupName = $mgName + SubscriptionId = $subId + SubscriptionName = $subName + ResourceGroup = $rgName + ResourceName_ResourceType = $resName + ExemptionName = $exemption.properties.DisplayName + ExemptionDescription = $exemption.properties.Description + Category = $exemption.properties.exemptionCategory + ExpiresOn_UTC = $exemptionExpiresOn + ExemptionId = $exemption.Id + PolicyAssignmentId = $exemption.properties.policyAssignmentId + PolicyType = $policyType + Policy = $policyClear + PoliciesTotalCount = $policiesTotalCount + PoliciesExemptedCount = $policiesExemptedCSVCount + PoliciesExempted = $policiesExemptedCSV + CreatedBy = "$($exemption.systemData.createdBy) ($($exemption.systemData.createdByType))" + CreatedAt = $exemption.systemData.createdAt.ToString('yyyy-MM-dd HH:mm:ss') + LastModifiedBy = "$($exemption.systemData.lastModifiedBy) ($($exemption.systemData.lastModifiedByType))" + LastModifiedAt = $exemption.systemData.lastModifiedAt.ToString('yyyy-MM-dd HH:mm:ss') + }) + } + + @" + + + + + + + + + + + + + + + + + + + + + + +"@ + } + + if (-not $NoCsvExport) { + Write-Host "Exporting PolicyExemptions CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyExemptions.csv'" + $exemptionData4CSVExport | Sort-Object -Property PolicyAssignmentId, Id | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyExemptions.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyExemptions) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeManagement Group IdManagement Group NameSubscriptionIdSubscription NameResourceGroupResourceName / ResourceTypeExemption nameExemption descriptionCategoryExpiresOn (UTC)Exemption IdPolicy AssignmentIdPolicy TypePolicyExempted Set PoliciesCreatedByCreatedAtLastModifiedByLastModifiedAt
$($exemptionScope)$($mgId)$($mgName -replace '<', '<' -replace '>', '>')$($subId)$($subName)$($rgName)$($resName)$($exemption.properties.DisplayName -replace '<', '<' -replace '>', '>')$($exemption.properties.Description -replace '<', '<' -replace '>', '>')$($exemption.properties.exemptionCategory -replace '<', '<' -replace '>', '>')$($exemptionExpiresOn)$($exemption.Id)$($exemption.properties.policyAssignmentId -replace '<', '<' -replace '>', '>')$($policyType)$($policy)$($policiesExempted)$($exemption.systemData.createdBy) ($($exemption.systemData.createdByType))$($exemption.systemData.createdAt.ToString('yyyy-MM-dd HH:mm:ss'))$($exemption.systemData.lastModifiedBy) ($($exemption.systemData.lastModifiedByType))$($exemption.systemData.lastModifiedAt.ToString('yyyy-MM-dd HH:mm:ss'))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($policyExemptionsCount) Policy exemptions

+"@) + } + #endregion SUMMARYPolicyExemptions + + #region SUMMARYPolicyAssignmentsOrphaned + Write-Host ' processing TenantSummary PolicyAssignments orphaned' + + if ($policyAssignmentsOrphanedCount -gt 0) { + $tfCount = $policyAssignmentsOrphanedCount + $htmlTableId = 'TenantSummary_policyAssignmentsOrphaned' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + + $htmlSUMMARYPolicyassignmentsOrphaned = $null + $htmlSUMMARYPolicyassignmentsOrphaned = foreach ($orphanedPolicyAssignment in $policyAssignmentsOrphaned | Sort-Object -Property PolicyAssignmentId) { + @" + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyassignmentsOrphaned) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy AssignmentIdPolicy/Set definition
$($orphanedPolicyAssignment.policyAssignmentId -replace '<', '<' -replace '>', '>')$($orphanedPolicyAssignment.PolicyDefinitionId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($policyAssignmentsOrphanedCount) Policy assignments orphaned

+"@) + } + #endregion SUMMARYPolicyAssignmentsOrphaned + + #region SUMMARYPolicyAssignmentsAll + $startSummaryPolicyAssignmentsAll = Get-Date + $allPolicyAssignments = ($policyBaseQuery).count + Write-Host " processing TenantSummary PolicyAssignments (all $allPolicyAssignments)" + + $script:arrayPolicyAssignmentsEnriched = [System.Collections.ArrayList]@() + $cnter = 0 + + #region PolicyAssignmentsRoleAssignmentMapping + $startPolicyAssignmentsRoleAssignmentMapping = Get-Date + Write-Host ' processing PolicyAssignmentsRoleAssignmentMapping' + $script:htPolicyAssignmentRoleAssignmentMapping = @{} + foreach ($roleassignmentId in ($htCacheAssignmentsRole).keys | Sort-Object) { + $roleAssignment = ($htCacheAssignmentsRole).($roleassignmentId).Assignment + + if ($htManagedIdentityForPolicyAssignment.($roleAssignment.ObjectId)) { + $mi = $htManagedIdentityForPolicyAssignment.($roleAssignment.ObjectId) + + #this + if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + } + + if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { + $roleDefinitionType = 'custom' + } + else { + $roleDefinitionType = 'builtin' + } + + $array = [System.Collections.ArrayList]@() + $null = $array.Add([PSCustomObject]@{ + roleassignmentId = $roleassignmentId + roleDefinitionId = $roleAssignment.RoleDefinitionId + roleDefinitionName = $roleAssignment.RoleDefinitionName + roleDefinitionType = $roleDefinitionType + }) + + #this + if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array + } + else { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + } + } + } + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + foreach ($roleassignmentId in ($htCacheAssignmentsRBACOnResourceGroupsAndResources).keys | Sort-Object) { + $roleAssignment = ($htCacheAssignmentsRBACOnResourceGroupsAndResources).($roleassignmentId) + + if ($htManagedIdentityForPolicyAssignment.($roleAssignment.ObjectId)) { + $mi = $htManagedIdentityForPolicyAssignment.($roleAssignment.ObjectId) + + #this + if (-not $htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower())) { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()) = @{} + } + + if (($htCacheDefinitionsRole).($roleAssignment.RoleDefinitionId).IsCustom) { + $roleDefinitionType = 'custom' + } + else { + $roleDefinitionType = 'builtin' + } + + $array = [System.Collections.ArrayList]@() + $null = $array.Add([PSCustomObject]@{ + roleassignmentId = $roleassignmentId + roleDefinitionId = $roleAssignment.RoleDefinitionId + roleDefinitionName = $roleAssignment.RoleDefinitionName + roleDefinitionType = $roleDefinitionType + }) + + #this + if ($htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments) { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments += $array + } + else { + $script:htPolicyAssignmentRoleAssignmentMapping.(($mi.policyAssignmentId).ToLower()).roleassignments = $array + } + } + } + } + $htPolicyAssignmentRoleAssignmentMappingCount = ($htPolicyAssignmentRoleAssignmentMapping.keys).Count + $endPolicyAssignmentsRoleAssignmentMapping = Get-Date + Write-Host " PolicyAssignmentsRoleAssignmentMapping processing duration: $((New-TimeSpan -Start $startPolicyAssignmentsRoleAssignmentMapping -End $endPolicyAssignmentsRoleAssignmentMapping).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmentsRoleAssignmentMapping -End $endPolicyAssignmentsRoleAssignmentMapping).TotalSeconds) seconds)" + #endregion PolicyAssignmentsRoleAssignmentMapping + + #region PolicyAssignmentsUniqueRelations + $startPolicyAssignmnetsUniqueRelations = Get-Date + Write-Host ' processing PolicyAssignmnetsUniqueRelations' + $htPolicyAssignmentRelatedRoleAssignments = @{} + $htPolicyAssignmentRelatedExemptions = @{} + + foreach ($policyAssignmentIdUnique in $policyBaseQueryUniqueAssignments) { + + #region relatedRoleAssignments + $relatedRoleAssignmentsArray = @() + $relatedRoleAssignmentsArrayClear = @() + if ($htPolicyAssignmentRoleAssignmentMappingCount -gt 0) { + if ($htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId)) { + foreach ($entry in $htPolicyAssignmentRoleAssignmentMapping.($policyAssignmentIdUnique.PolicyAssignmentId).roleassignments) { + if ($entry.roleDefinitionType -eq 'builtin') { + $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + } + else { + $relatedRoleAssignmentsArray += "$($entry.roleDefinitionName -replace '<', '<' -replace '>', '>') ($($entry.roleAssignmentId))" + } + $relatedRoleAssignmentsArrayClear += "$($entry.roleDefinitionName) ($($entry.roleAssignmentId))" + } + } + } + + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} + if (($relatedRoleAssignmentsArray).count -gt 0) { + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = ($relatedRoleAssignmentsArray | Sort-Object) -join "$CsvDelimiterOpposite " + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = ($relatedRoleAssignmentsArrayClear | Sort-Object) -join "$CsvDelimiterOpposite " + } + else { + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignments = 'none' + $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentIdUnique.PolicyAssignmentId).relatedRoleAssignmentsClear = 'none' + } + #endregion relatedRoleAssignments + + #region exemptions + $arrayExemptions = @() + foreach ($exemptionId in $htPolicyAssignmentExemptions.keys) { + if ($htPolicyAssignmentExemptions.($exemptionId).exemption.properties.policyAssignmentId -eq $policyAssignmentIdUnique.PolicyAssignmentId) { + $arrayExemptions += $htPolicyAssignmentExemptions.($exemptionId).exemption + if (-not $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId)) { + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId) = @{} + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount = 1 + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions + } + else { + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptionsCount += 1 + $htPolicyAssignmentRelatedExemptions.($policyAssignmentIdUnique.PolicyAssignmentId).exemptions = $arrayExemptions + } + } + } + #endregion exemptions + } + $endPolicyAssignmnetsUniqueRelations = Get-Date + Write-Host " PolicyAssignmnetsUniqueRelations processing duration: $((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmnetsUniqueRelations -End $endPolicyAssignmnetsUniqueRelations).TotalSeconds) seconds)" + #endregion PolicyAssignmentsUniqueRelations + + #region PolicyAssignmentsAllCreateEnriched + $startPolicyAssignmentsAllCreateEnriched = Get-Date + Write-Host ' processing PolicyAssignmentsAllCreateEnriched' + foreach ($policyAssignmentAll in $policyBaseQuery) { + + $cnter++ + if ($cnter % 1000 -eq 0) { + $etappeSummaryPolicyAssignmentsAll = Get-Date + Write-Host " $cnter of $allPolicyAssignments PolicyAssignments processed: $((New-TimeSpan -Start $startSummaryPolicyAssignmentsAll -End $etappeSummaryPolicyAssignmentsAll).TotalSeconds) seconds" + } + + #region AzAdvertizerLinkOrNot + if ($policyAssignmentAll.PolicyType -eq 'builtin') { + if ($policyAssignmentAll.PolicyVariant -eq 'Policy') { + $azaLinkOrNot = "$($policyAssignmentAll.Policy)" + } + else { + $azaLinkOrNot = "$($policyAssignmentAll.Policy)" + } + } + else { + $azaLinkOrNot = $policyAssignmentAll.Policy + } + #endregion AzAdvertizerLinkOrNot + + #region excludedScope + $excludedScope = 'false' + if (($policyAssignmentAll.PolicyAssignmentNotScopes).count -gt 0) { + foreach ($policyAssignmentNotScope in $policyAssignmentAll.PolicyAssignmentNotScopes) { + if (-not [String]::IsNullOrEmpty($policyAssignmentAll.subscriptionId)) { + if ($htSubscriptionsMgPath.($policyAssignmentAll.subscriptionId).path -contains ($($policyAssignmentNotScope -replace '/subscriptions/' -replace '/providers/Microsoft.Management/managementGroups/'))) { + $excludedScope = 'true' + } + } + else { + if ($htManagementGroupsMgPath.($policyAssignmentAll.MgId).path -contains ($($policyAssignmentNotScope -replace '/providers/Microsoft.Management/managementGroups/'))) { + $excludedScope = 'true' + } + } + } + } + #endregion excludedScope + + #region exemptions + $exemptionScope = 'false' + if ($htPolicyAssignmentRelatedExemptions.($policyAssignmentAll.PolicyAssignmentId)) { + foreach ($exemption in $htPolicyAssignmentRelatedExemptions.($policyAssignmentAll.PolicyAssignmentId).exemptions) { + if ($exemption.properties.expiresOn) { + if ($exemption.properties.expiresOn -gt (Get-Date).ToUniversalTime()) { + if (-not [String]::IsNullOrEmpty($policyAssignmentAll.subscriptionId)) { + if ($htSubscriptionsMgPath.($policyAssignmentAll.subscriptionId).path -contains ($(($exemption.Id -split '/providers/Microsoft.Authorization/policyExemptions/')[0] -replace '/subscriptions/' -replace '/providers/Microsoft.Management/managementGroups/'))) { + $exemptionScope = 'true' + } + } + else { + if ($htManagementGroupsMgPath.($policyAssignmentAll.MgId).path -contains ($(($exemption.Id -split '/providers/Microsoft.Authorization/policyExemptions/')[0] -replace '/subscriptions/' -replace '/providers/Microsoft.Management/managementGroups/'))) { + $exemptionScope = 'true' + } + } + } + else { + #Write-Host "$($exemption.Id) $($exemption.properties.expiresOn) $((Get-Date).ToUniversalTime()) expired" + } + } + else { + #same code as above / function? + if (-not [String]::IsNullOrEmpty($policyAssignmentAll.subscriptionId)) { + if ($htSubscriptionsMgPath.($policyAssignmentAll.subscriptionId).path -contains ($(($exemption.Id -split '/providers/Microsoft.Authorization/policyExemptions/')[0] -replace '/subscriptions/' -replace '/providers/Microsoft.Management/managementGroups/'))) { + $exemptionScope = 'true' + } + } + else { + if ($htManagementGroupsMgPath.($policyAssignmentAll.MgId).path -contains ($(($exemption.Id -split '/providers/Microsoft.Authorization/policyExemptions/')[0] -replace '/subscriptions/' -replace '/providers/Microsoft.Management/managementGroups/'))) { + $exemptionScope = 'true' + } + } + } + } + } + #endregion exemptions + + #region inheritance + if ($policyAssignmentAll.PolicyAssignmentId -like '/providers/Microsoft.Management/managementGroups/*') { + if (-not [String]::IsNullOrEmpty($policyAssignmentAll.SubscriptionId)) { + $scope = "inherited $($policyAssignmentAll.PolicyAssignmentScope -replace '.*/')" + } + else { + if (($policyAssignmentAll.PolicyAssignmentScope -replace '.*/') -eq $policyAssignmentAll.MgId) { + $scope = 'thisScope Mg' + } + else { + $scope = "inherited $($policyAssignmentAll.PolicyAssignmentScope -replace '.*/')" + } + } + } + + if ($policyAssignmentAll.PolicyAssignmentId -like '/subscriptions/*' -and $policyAssignmentAll.PolicyAssignmentId -notlike '/subscriptions/*/resourcegroups/*') { + $scope = 'thisScope Sub' + } + + if ($policyAssignmentAll.PolicyAssignmentId -like '/subscriptions/*/resourcegroups/*') { + $scope = 'thisScope Sub RG' + } + #endregion inheritance + + #region effect + $effect = 'unknown' + if ($policyAssignmentAll.PolicyVariant -eq 'Policy') { + + $test0 = $policyAssignmentAll.PolicyAssignmentParameters.effect.value + if ($test0) { + $effect = $test0 + } + else { + $test1 = $policyAssignmentAll.PolicyDefinitionEffectDefault + if ($test1 -ne 'n/a') { + $effect = $test1 + } + $test2 = $policyAssignmentAll.PolicyDefinitionEffectFixed + if ($test2 -ne 'n/a') { + $effect = $test2 + } + } + } + else { + $effect = 'n/a' + } + #endregion effect + + #region mgOrSubOrRG + if ([String]::IsNullOrEmpty($policyAssignmentAll.SubscriptionId)) { + $mgOrSubOrRG = 'Mg' + } + else { + if ($scope -like '*RG') { + $mgOrSubOrRG = 'RG' + } + else { + $mgOrSubOrRG = 'Sub' + } + } + #endregion mgOrSubOrRG + + #region category + if ([string]::IsNullOrEmpty($policyAssignmentAll.PolicyCategory)) { + $policyCategory = 'n/a' + } + else { + $policyCategory = $policyAssignmentAll.PolicyCategory + } + #endregion category + + #region createdByUpdatedBy + #createdBy + if ($policyAssignmentAll.PolicyAssignmentCreatedBy) { + $createdBy = $policyAssignmentAll.PolicyAssignmentCreatedBy + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + } + } + else { + $createdBy = '' + } + + #UpdatedBy + if ($policyAssignmentAll.PolicyAssignmentUpdatedBy) { + $updatedBy = $policyAssignmentAll.PolicyAssignmentUpdatedBy + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedBy)) { + $updatedBy = $htIdentitiesWithRoleAssignmentsUnique.($updatedBy).details + } + } + else { + $updatedBy = '' + } + #endregion createdByUpdatedBy + + #region policyAssignmentNotScopes + if ($policyAssignmentAll.PolicyAssignmentNotScopes) { + $policyAssignmentNotScopes = $policyAssignmentAll.PolicyAssignmentNotScopes -join $CsvDelimiterOpposite + } + else { + $policyAssignmentNotScopes = 'n/a' + } + #endregion policyAssignmentNotScopes + + #region + $policyAssignmentMI = '' + if ($htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentAll.PolicyAssignmentId)) { + $hlp = $htPolicyAssignmentRelatedRoleAssignments.($policyAssignmentAll.PolicyAssignmentId) + $relatedRoleAssignments = $hlp.relatedRoleAssignments + $relatedRoleAssignmentsClear = $hlp.relatedRoleAssignmentsClear + if ($htManagedIdentityDisplayName.("$($policyAssignmentAll.PolicyAssignmentId -replace '.*/')_$($policyAssignmentAll.PolicyAssignmentId)")) { + $hlp = $htManagedIdentityDisplayName.("$($policyAssignmentAll.PolicyAssignmentId -replace '.*/')_$($policyAssignmentAll.PolicyAssignmentId)") + $policyAssignmentMI = "$($hlp.displayname) (SPObjId: $($hlp.id))" + } + } + #endregion + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + #region policyCompliance + $policyAssignmentIdToLower = ($policyAssignmentAll.policyAssignmentId).ToLower() + + #mg + if ([String]::IsNullOrEmpty($policyAssignmentAll.subscriptionId)) { + if (($htCachePolicyComplianceResponseTooLargeMG).($policyAssignmentAll.MgId)) { + $NonCompliantPolicies = 'skipped' + $CompliantPolicies = 'skipped' + $NonCompliantResources = 'skipped' + $CompliantResources = 'skipped' + $ConflictingResources = 'skipped' + } + else { + $compliance = ($htCachePolicyComplianceMG).($policyAssignmentAll.MgId).($policyAssignmentIdToLower) + $NonCompliantPolicies = $compliance.NonCompliantPolicies + $CompliantPolicies = $compliance.CompliantPolicies + $NonCompliantResources = $compliance.NonCompliantResources + $CompliantResources = $compliance.CompliantResources + $ConflictingResources = $compliance.ConflictingResources + + if (!$NonCompliantPolicies) { + $NonCompliantPolicies = 0 + } + if (!$CompliantPolicies) { + $CompliantPolicies = 0 + } + if (!$NonCompliantResources) { + $NonCompliantResources = 0 + } + if (!$CompliantResources) { + $CompliantResources = 0 + } + if (!$ConflictingResources) { + $ConflictingResources = 0 + } + } + } + + #sub/rg + if (-not [String]::IsNullOrEmpty($policyAssignmentAll.subscriptionId)) { + if (($htCachePolicyComplianceResponseTooLargeSUB).($policyAssignmentAll.SubscriptionId)) { + $NonCompliantPolicies = 'skipped' + $CompliantPolicies = 'skipped' + $NonCompliantResources = 'skipped' + $CompliantResources = 'skipped' + $ConflictingResources = 'skipped' + } + else { + $compliance = ($htCachePolicyComplianceSUB).($policyAssignmentAll.SubscriptionId).($policyAssignmentIdToLower) + $NonCompliantPolicies = $compliance.NonCompliantPolicies + $CompliantPolicies = $compliance.CompliantPolicies + $NonCompliantResources = $compliance.NonCompliantResources + $CompliantResources = $compliance.CompliantResources + $ConflictingResources = $compliance.ConflictingResources + + if (!$NonCompliantPolicies) { + $NonCompliantPolicies = 0 + } + if (!$CompliantPolicies) { + $CompliantPolicies = 0 + } + if (!$NonCompliantResources) { + $NonCompliantResources = 0 + } + if (!$CompliantResources) { + $CompliantResources = 0 + } + if (!$ConflictingResources) { + $ConflictingResources = 0 + } + } + } + #endregion policyCompliance + + $null = $script:arrayPolicyAssignmentsEnriched.Add([PSCustomObject]@{ + Level = $policyAssignmentAll.Level + MgId = $policyAssignmentAll.MgId + MgName = $policyAssignmentAll.MgName + MgParentId = $policyAssignmentAll.MgParentId + MgParentName = $policyAssignmentAll.MgParentName + subscriptionId = $policyAssignmentAll.SubscriptionId + subscriptionName = $policyAssignmentAll.Subscription + PolicyAssignmentId = (($policyAssignmentAll.PolicyAssignmentId).ToLower()) + PolicyAssignmentScopeName = $policyAssignmentAll.PolicyAssignmentScopeName + PolicyAssignmentDisplayName = $policyAssignmentAll.PolicyAssignmentDisplayName + PolicyAssignmentDescription = $policyAssignmentAll.PolicyAssignmentDescription + PolicyAssignmentEnforcementMode = $policyAssignmentAll.PolicyAssignmentEnforcementMode + PolicyAssignmentNonComplianceMessages = $policyAssignmentAll.PolicyAssignmentNonComplianceMessages + PolicyAssignmentNotScopes = $policyAssignmentNotScopes + PolicyAssignmentParameters = $policyAssignmentAll.PolicyAssignmentParametersFormated + PolicyAssignmentMI = $policyAssignmentMI + AssignedBy = $policyAssignmentAll.PolicyAssignmentAssignedBy + CreatedOn = $policyAssignmentAll.PolicyAssignmentCreatedOn + CreatedBy = $createdBy + UpdatedOn = $policyAssignmentAll.PolicyAssignmentUpdatedOn + UpdatedBy = $updatedBy + Effect = $effect + PolicyName = $azaLinkOrNot + PolicyNameClear = $policyAssignmentAll.Policy + PolicyAvailability = $policyAssignmentAll.PolicyAvailability + PolicyDescription = $policyAssignmentAll.PolicyDescription + PolicyId = $policyAssignmentAll.PolicyDefinitionId + PolicyVariant = $policyAssignmentAll.PolicyVariant + PolicyType = $policyAssignmentAll.PolicyType + PolicyIsALZ = $policyAssignmentAll.PolicyIsALZ + PolicyCategory = $policyCategory + Inheritance = $scope + ExcludedScope = $excludedScope + RelatedRoleAssignments = $relatedRoleAssignments + RelatedRoleAssignmentsClear = $relatedRoleAssignmentsClear + mgOrSubOrRG = $mgOrSubOrRG + NonCompliantPolicies = $NonCompliantPolicies + CompliantPolicies = $CompliantPolicies + NonCompliantResources = $NonCompliantResources + CompliantResources = $CompliantResources + ConflictingResources = $ConflictingResources + ExemptionScope = $exemptionScope + }) + } + else { + $null = $script:arrayPolicyAssignmentsEnriched.Add([PSCustomObject]@{ + Level = $policyAssignmentAll.Level + MgId = $policyAssignmentAll.MgId + MgName = $policyAssignmentAll.MgName + MgParentId = $policyAssignmentAll.MgParentId + MgParentName = $policyAssignmentAll.MgParentName + subscriptionId = $policyAssignmentAll.SubscriptionId + subscriptionName = $policyAssignmentAll.Subscription + PolicyAssignmentId = (($policyAssignmentAll.PolicyAssignmentId).ToLower()) + PolicyAssignmentScopeName = $policyAssignmentAll.PolicyAssignmentScopeName + PolicyAssignmentDisplayName = $policyAssignmentAll.PolicyAssignmentDisplayName + PolicyAssignmentDescription = $policyAssignmentAll.PolicyAssignmentDescription + PolicyAssignmentEnforcementMode = $policyAssignmentAll.PolicyAssignmentEnforcementMode + PolicyAssignmentNonComplianceMessages = $policyAssignmentAll.PolicyAssignmentNonComplianceMessages + PolicyAssignmentNotScopes = $policyAssignmentNotScopes + PolicyAssignmentParameters = $policyAssignmentAll.PolicyAssignmentParametersFormated + PolicyAssignmentMI = $policyAssignmentMI + AssignedBy = $policyAssignmentAll.PolicyAssignmentAssignedBy + CreatedOn = $policyAssignmentAll.PolicyAssignmentCreatedOn + CreatedBy = $createdBy + UpdatedOn = $policyAssignmentAll.PolicyAssignmentUpdatedOn + UpdatedBy = $updatedBy + Effect = $effect + PolicyName = $azaLinkOrNot + PolicyNameClear = $policyAssignmentAll.Policy + PolicyAvailability = $policyAssignmentAll.PolicyAvailability + PolicyDescription = $policyAssignmentAll.PolicyDescription + PolicyId = $policyAssignmentAll.PolicyDefinitionId + PolicyVariant = $policyAssignmentAll.PolicyVariant + PolicyType = $policyAssignmentAll.PolicyType + PolicyIsALZ = $policyAssignmentAll.PolicyIsALZ + PolicyCategory = $policyCategory + Inheritance = $scope + ExcludedScope = $excludedScope + RelatedRoleAssignments = $relatedRoleAssignments + RelatedRoleAssignmentsClear = $relatedRoleAssignmentsClear + mgOrSubOrRG = $mgOrSubOrRG + ExemptionScope = $exemptionScope + }) + } + } + $EndPolicyAssignmentsAllCreateEnriched = Get-Date + Write-Host " PolicyAssignmentsAllCreateEnriched processing duration: $((New-TimeSpan -Start $startPolicyAssignmentsAllCreateEnriched -End $EndPolicyAssignmentsAllCreateEnriched).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyAssignmentsAllCreateEnriched -End $EndPolicyAssignmentsAllCreateEnriched).TotalSeconds) seconds)" + #endregion PolicyAssignmentsAllCreateEnriched + + #region PolicyAssignmentsAllResolveIdentities + Write-Host ' processing unresoved Identities (createdBy/updatedBy)' + $startUnResolvedIdentitiesCreatedByUpdatedByPolicy = Get-Date + + $createdByNotResolved = ($arrayPolicyAssignmentsEnriched.where( { -not [string]::IsNullOrEmpty($_.CreatedBy) -and $_.CreatedBy -notlike 'ObjectType:*' })).CreatedBy | Sort-Object -Unique + $updatedByNotResolved = ($arrayPolicyAssignmentsEnriched.where( { -not [string]::IsNullOrEmpty($_.UpdatedBy) -and $_.UpdatedBy -notlike 'ObjectType:*' })).UpdatedBy | Sort-Object -Unique + + $htNonResolvedIdentitiesPolicy = @{} + foreach ($createdByNotResolvedEntry in $createdByNotResolved) { + if (-not $htNonResolvedIdentitiesPolicy.($createdByNotResolvedEntry)) { + $htNonResolvedIdentitiesPolicy.($createdByNotResolvedEntry) = @{} + } + } + foreach ($updatedByNotResolvedEntry in $updatedByNotResolved) { + if (-not $htNonResolvedIdentitiesPolicy.($updatedByNotResolvedEntry)) { + $htNonResolvedIdentitiesPolicy.($updatedByNotResolvedEntry) = @{} + } + } + + $htNonResolvedIdentitiesPolicyCount = $htNonResolvedIdentitiesPolicy.Count + if ($htNonResolvedIdentitiesPolicyCount -gt 0) { + Write-Host " $htNonResolvedIdentitiesPolicyCount unresolved identities that created/updated a Policy assignment (createdBy/updatedBy)" + $arrayUnresolvedIdentities = @() + $arrayUnresolvedIdentities = foreach ($unresolvedIdentity in $htNonResolvedIdentitiesPolicy.keys) { + if (-not [string]::IsNullOrEmpty($unresolvedIdentity)) { + $unresolvedIdentity + } + } + $arrayUnresolvedIdentitiesCount = $arrayUnresolvedIdentities.Count + Write-Host " $arrayUnresolvedIdentitiesCount unresolved identities that have a value" + if ($arrayUnresolvedIdentitiesCount.Count -gt 0) { + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 1000 + $ObjectBatch = $arrayUnresolvedIdentities | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + $ObjectBatchCount = ($ObjectBatch | Measure-Object).Count + $batchCnt = 0 + + $script:htResolvedIdentitiesPolicy = @{} + + foreach ($batch in $ObjectBatch) { + $batchCnt++ + + $nonResolvedIdentitiesToCheck = '"{0}"' -f ($batch.Group.where({ testGuid $_ }) -join '","') + Write-Host " IdentitiesToCheck: Batch #$batchCnt/$($ObjectBatchCount) ($(($batch.Group).Count))" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/directoryObjects/getByIds" + $method = 'POST' + $body = @" + { + "ids":[$($nonResolvedIdentitiesToCheck)] + } +"@ + + function resolveIdentitiesPolicy($currentTask) { + $resolvedIdentities = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask + $resolvedIdentitiesCount = $resolvedIdentities.Count + Write-Host " $resolvedIdentitiesCount identities resolved" + if ($resolvedIdentitiesCount -gt 0) { + foreach ($resolvedIdentity in $resolvedIdentities) { + if (-not $htResolvedIdentitiesPolicy.($resolvedIdentity.id)) { + $script:htResolvedIdentitiesPolicy.($resolvedIdentity.id) = @{} + if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + if ($resolvedIdentity.servicePrincipalType -eq 'ManagedIdentity') { + $miType = 'unknown' + foreach ($altName in $resolvedIdentity.alternativeNames) { + if ($altName -like 'isExplicit=*') { + $splitAltName = $altName.split('=') + if ($splitAltName[1] -eq 'true') { + $miType = 'Usr' + } + if ($splitAltName[1] -eq 'false') { + $miType = 'Sys' + } + } + } + $sptype = "MI $miType" + $custObjectType = "ObjectType: SP $sptype, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (rp)" + } + else { + if ($resolvedIdentity.servicePrincipalType -eq 'Application') { + $sptype = 'App' + if ($resolvedIdentity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { + $custObjectType = "ObjectType: SP $sptype INT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (rp)" + } + else { + $custObjectType = "ObjectType: SP $sptype EXT, ObjectDisplayName: $($resolvedIdentity.displayName), ObjectSignInName: n/a, ObjectId: $($resolvedIdentity.id) (rp)" + } + } + else { + Write-Host "* * * Unexpected IdentityType $($resolvedIdentity.servicePrincipalType)" + } + } + $script:htResolvedIdentitiesPolicy.($resolvedIdentity.id).custObjectType = $custObjectType + $script:htResolvedIdentitiesPolicy.($resolvedIdentity.id).obj = $resolvedIdentity + } + + if ($resolvedIdentity.'@odata.type' -eq '#microsoft.graph.user') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData) { + $hlpObjectDisplayName = 'scrubbed' + $hlpObjectSigninName = 'scrubbed' + } + else { + $hlpObjectDisplayName = $resolvedIdentity.displayName + $hlpObjectSigninName = $resolvedIdentity.userPrincipalName + } + $custObjectType = "ObjectType: User, ObjectDisplayName: $hlpObjectDisplayName, ObjectSignInName: $hlpObjectSigninName, ObjectId: $($resolvedIdentity.id) (rp)" + + $script:htResolvedIdentitiesPolicy.($resolvedIdentity.id).custObjectType = $custObjectType + $script:htResolvedIdentitiesPolicy.($resolvedIdentity.id).obj = $resolvedIdentity + } + + if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { + Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow + } + } + } + } + } + resolveIdentitiesPolicy -currentTask 'resolveObjectbyId PolicyAssignment #1' + } + + foreach ($policyAssignment in $script:arrayPolicyAssignmentsEnriched.where( { -not [string]::IsNullOrEmpty($_.CreatedBy) -and $_.CreatedBy -notlike 'ObjectType*' })) { + if ($htResolvedIdentitiesPolicy.($policyAssignment.CreatedBy)) { + $policyAssignment.CreatedBy = $htResolvedIdentitiesPolicy.($policyAssignment.CreatedBy).custObjectType + } + } + + foreach ($policyAssignment in $script:arrayPolicyAssignmentsEnriched.where( { -not [string]::IsNullOrEmpty($_.UpdatedBy) -and $_.UpdatedBy -notlike 'ObjectType*' })) { + if ($htResolvedIdentitiesPolicy.($policyAssignment.UpdatedBy)) { + $policyAssignment.UpdatedBy = $htResolvedIdentitiesPolicy.($policyAssignment.UpdatedBy).custObjectType + } + } + } + } + + $endUnResolvedIdentitiesCreatedByUpdatedByPolicy = Get-Date + Write-Host " UnresolvedIdentities (createdBy/updatedBy) duration: $((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedByUpdatedByPolicy -End $endUnResolvedIdentitiesCreatedByUpdatedByPolicy).TotalMinutes) minutes ($((New-TimeSpan -Start $startUnResolvedIdentitiesCreatedByUpdatedByPolicy -End $endUnResolvedIdentitiesCreatedByUpdatedByPolicy).TotalSeconds) seconds)" + #endregion PolicyAssignmentsAllResolveIdentities + + $script:arrayPolicyAssignmentsEnrichedGroupedBySubscription = $arrayPolicyAssignmentsEnriched | Group-Object -Property subscriptionId + $script:arrayPolicyAssignmentsEnrichedGroupedByManagementGroup = $arrayPolicyAssignmentsEnriched | Group-Object -Property MgId + + #region policyAssignmentsAllHTML + Write-Host ' processing SummaryPolicyAssignmentsAllHTML' + $startSummaryPolicyAssignmentsAllHTML = Get-Date + if (($arrayPolicyAssignmentsEnriched).count -gt 0) { + + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_PolicyAssignments" + Write-Host " Exporting PolicyAssignments CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + if ($CsvExportUseQuotesAsNeeded) { + $arrayPolicyAssignmentsEnriched | Sort-Object -Property Level, MgId, SubscriptionId, PolicyAssignmentId | Select-Object -ExcludeProperty PolicyName, RelatedRoleAssignments | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded + } + else { + $arrayPolicyAssignmentsEnriched | Sort-Object -Property Level, MgId, SubscriptionId, PolicyAssignmentId | Select-Object -ExcludeProperty PolicyName, RelatedRoleAssignments | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + + $policyAssignmentsUniqueCount = ($arrayPolicyAssignmentsEnriched | Sort-Object -Property PolicyAssignmentId -Unique).count + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].PolicyAtScopeOnly) { + $policyAssignmentsCount = $policyAssignmentsUniqueCount + $tfCount = $policyAssignmentsCount + } + else { + $policyAssignmentsCount = ($arrayPolicyAssignmentsEnriched).count + $tfCount = $policyAssignmentsCount + } + + if ($tfCount -gt $HtmlTableRowsLimit) { + Write-Host " !Skipping TenantSummary PolicyAssignments HTML processing as $tfCount lines is exceeding the critical rows limit of $HtmlTableRowsLimit" -ForegroundColor Yellow + [void]$htmlTenantSummary.AppendLine(@" + +
+ Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
+ You can adjust the html row limit by using parameter -HtmlTableRowsLimit
+ You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
+ Check the parameters documentation Azure Governance Visualizer docs +
+"@) + } + else { + + $htmlTableId = 'TenantSummary_policyAssignmentsAll' + $noteOrNot = '' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma
+*Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience + + + + + + + + + + + + + + + + + + + + + + +"@) + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + [void]$htmlTenantSummary.AppendLine(@' + + + + + +'@) + } + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + +"@) + + $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlTenantSummary = [System.Text.StringBuilder]::new() + $htmlSummaryPolicyAssignmentsAll = $null + $startloop = Get-Date + + $htmlSummaryPolicyAssignmentsAll = foreach ($policyAssignment in $arrayPolicyAssignmentsEnriched | Sort-Object -Property Level, MgName, MgId, SubscriptionName, SubscriptionId, PolicyAssignmentId) { + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].PolicyAtScopeOnly) { + if ($policyAssignment.Inheritance -like 'inherited *' -and $policyAssignment.MgParentId -ne "'upperScopes'") { + continue + } + } + if ($policyAssignment.PolicyType -eq 'Custom') { + $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') + } + else { + $policyName = $policyAssignment.PolicyName + } + @" + + + + + + + + + + + + + + + + + + + + +"@ + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + @" + + + + + +"@ + } + + @" + + + + + + + + + + + +"@ + } + + $endloop = Get-Date + Write-Host " html foreach loop duration: $((New-TimeSpan -Start $startloop -End $endloop).TotalSeconds) seconds" + + $start = Get-Date + [void]$htmlTenantSummary.AppendLine($htmlSummaryPolicyAssignmentsAll) + $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlTenantSummary = [System.Text.StringBuilder]::new() + $end = Get-Date + Write-Host " html append file duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" + + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeManagement Group IdManagement Group NameSubscriptionIdSubscription NameInheritanceScopeExcludedExemption appliesPolicy/Set DisplayNamePolicy/Set DescriptionPolicy/SetIdPolicy/SetTypeCategoryALZEffectParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotManaged IdentityAssignment DisplayNameAssignment DescriptionAssignmentIdAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.mgOrSubOrRG)$($policyAssignment.MgId)$($policyAssignment.MgName -replace '<', '<' -replace '>', '>')$($policyAssignment.SubscriptionId)$($policyAssignment.SubscriptionName)$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyAssignment.ExemptionScope)$($policyName)$($policyAssignment.PolicyDescription -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyId -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyVariant)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyIsALZ)$($policyAssignment.Effect)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages -replace '<', '<' -replace '>', '>')$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentMI)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentDescription -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
+
+ +"@) + } + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($arrayPolicyAssignmentsEnriched).count) Policy assignments

+"@) + } + $endSummaryPolicyAssignmentsAllHTML = Get-Date + Write-Host " SummaryPolicyAssignmentsAllHTML duration: $((New-TimeSpan -Start $startSummaryPolicyAssignmentsAllHTML -End $endSummaryPolicyAssignmentsAllHTML).TotalMinutes) minutes ($((New-TimeSpan -Start $startSummaryPolicyAssignmentsAllHTML -End $endSummaryPolicyAssignmentsAllHTML).TotalSeconds) seconds)" + #endregion policyAssignmentsAllHTML + $endSummaryPolicyAssignmentsAll = Get-Date + Write-Host " SummaryPolicyAssignmentsAll duration: $((New-TimeSpan -Start $startSummaryPolicyAssignmentsAll -End $endSummaryPolicyAssignmentsAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startSummaryPolicyAssignmentsAll -End $endSummaryPolicyAssignmentsAll).TotalSeconds) seconds)" + #endregion SUMMARYPolicyAssignmentsAll + + #region SUMMARYPolicyRemediation + Write-Host ' processing TenantSummary Policy Remediation' + + if ($arrayRemediatable.Count -gt 0) { + $tfCount = $arrayRemediatable.Count + $nonCompliantResourcesTotal = ($arrayRemediatable.nonCompliantResourcesCount | Measure-Object -Sum).Sum + $htmlTableId = 'TenantSummary_PolicyRemediation' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + +"@) + + $htmlSUMMARYPolicyRemediation = $null + $arrayRemediatableSorted = $arrayRemediatable | Sort-Object -Property nonCompliantResourcesCount, policySetPolicyDefinitionReferenceId, policyDefinitionId, policyAssignmentId -Descending + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_PolicyRemediation" + Write-Host " Exporting PolicyRemediation CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $arrayRemediatableSorted | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + $htmlSUMMARYPolicyRemediation = foreach ($entry in $arrayRemediatableSorted) { + + if ($entry.policyDefinitionType -eq 'builtin') { + $pd = "$($entry.policyDefinitionDisplayName) ($($entry.policyDefinitionName))" + } + else { + $pd = "$($entry.policyDefinitionDisplayName) ($($entry.policyDefinitionName))" + } + + if ($entry.policySetDefinitionType -ne 'n/a') { + if ($entry.policySetDefinitionType -eq 'builtIn') { + $psd = "$($entry.policySetDefinitionDisplayName) ($($entry.policySetDefinitionName))" + } + else { + $psd = "$($entry.policySetDefinitionDisplayName) ($($entry.policySetDefinitionName))" + } + } + else { + $psd = $entry.policySetDefinitionType + } + + @" + + + + + + + + + + + + + + + + +"@ + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyRemediation) + [void]$htmlTenantSummary.AppendLine(@" + +
Assignment Scope TypeAssignment ScopeAssignment IdAssignment DisplayNameAssignment Policy/SetEffectPolicy definition idPolicy definition displayNamePolicy definition typePolicy definition refIdPolicySet definition idPolicySet definition displayNamePolicySet definition typeNonCompliant resources
$($entry.policyAssignmentScopeType)$($entry.policyAssignmentScope)$($entry.policyAssignmentId)$($entry.policyAssignmentDisplayName)$($entry.policyAssignmentPolicyOrPolicySet)$($entry.effect)$($entry.policyDefinitionId)$($pd)$($entry.policyDefinitionType)$($entry.policySetPolicyDefinitionReferenceId)$($entry.policySetDefinitionId)$($psd)$($entry.policySetDefinitionType)$($entry.nonCompliantResourcesCount)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Policies to remediate

+'@) + } + #endregion SUMMARYPolicyRemediation + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryPolicy + + showMemoryUsage + + #region tenantSummaryRBAC + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + #region SUMMARYtenanttotalcustomroles + Write-Host ' processing TenantSummary Custom Roles' + if ($tenantCustomRolesCount -gt $LimitRBACCustomRoleDefinitionsTenant * ($LimitCriticalPercentage / 100)) { + $faimage = "" + } + else { + $faimage = "" + } + + if ($tenantCustomRolesCount -gt 0) { + $tfCount = $tenantCustomRolesCount + $htmlTableId = 'TenantSummary_customRoles' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYtenanttotalcustomroles = $null + $htmlSUMMARYtenanttotalcustomroles = foreach ($tenantCustomRole in $tenantCustomRoles | Sort-Object @{Expression = { $_.Name } }, @{Expression = { $_.Id } }) { + $cachedTenantCustomRole = ($htCacheDefinitionsRole).($tenantCustomRole.Id) + if (-not [string]::IsNullOrEmpty($cachedTenantCustomRole.DataActions) -or -not [string]::IsNullOrEmpty($cachedTenantCustomRole.NotDataActions)) { + $roleManageData = 'true' + } + else { + $roleManageData = 'false' + } + + if (-not [string]::IsNullOrEmpty($cachedTenantCustomRole.Json.properties.createdBy)) { + $createdBy = $cachedTenantCustomRole.Json.properties.createdBy + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + } + } + else { + $createdBy = 'IsNullOrEmpty' + } + + $createdOn = $cachedTenantCustomRole.Json.properties.createdOn + $createdOnFormated = $createdOn + $updatedOn = $cachedTenantCustomRole.Json.properties.updatedOn + if ($updatedOn -eq $createdOn) { + $updatedOnFormated = '' + $updatedByRemoveNoiseOrNot = '' + } + else { + $updatedOnFormated = $updatedOn + if (-not [string]::IsNullOrEmpty($cachedTenantCustomRole.Json.properties.updatedBy)) { + $updatedByRemoveNoiseOrNot = $cachedTenantCustomRole.Json.properties.updatedBy + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedByRemoveNoiseOrNot)) { + $updatedByRemoveNoiseOrNot = $htIdentitiesWithRoleAssignmentsUnique.($updatedByRemoveNoiseOrNot).details + } + } + else { + $updatedByRemoveNoiseOrNot = 'IsNullOrEmpty' + } + } + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYtenanttotalcustomroles) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdAssignable ScopesDataCreatedOnCreatedByUpdatedOnUpdatedBy
$($cachedTenantCustomRole.Name -replace '<', '<' -replace '>', '>')$($cachedTenantCustomRole.Id)$(($cachedTenantCustomRole.AssignableScopes).count) ($($cachedTenantCustomRole.AssignableScopes -join "$CsvDelimiterOpposite "))$($roleManageData)$createdOnFormated$createdBy$updatedOnFormated$updatedByRemoveNoiseOrNot
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$tenantCustomRolesCount Custom Role definitions ($scopeNamingSummary)

+"@) + } + #endregion SUMMARYtenanttotalcustomroles + + #region SUMMARYOrphanedCustomRoles + $startSUMMARYOrphanedCustomRoles = Get-Date + Write-Host ' processing TenantSummary Custom Roles orphaned' + if ($getMgParentName -eq 'Tenant Root') { + $arrayCustomRolesOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + + if (($tenantCustomRoles).count -gt 0) { + $mgSubRoleAssignmentsArrayRoleDefinitionIdUnique = $mgSubRoleAssignmentsArrayFromHTValues.RoleDefinitionId | Sort-Object -Unique + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + $rgResRoleAssignmentsArrayRoleDefinitionIdUnique = $rgResRoleAssignmentsArrayFromHTValues.RoleDefinitionId | Sort-Object -Unique + } + foreach ($customRoleAll in $tenantCustomRoles) { + $roleIsUsed = $false + if (($mgSubRoleAssignmentsArrayRoleDefinitionIdUnique) -contains ($customRoleAll.Id)) { + $roleIsUsed = $true + } + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if ($roleIsUsed -eq $false) { + if (($rgResRoleAssignmentsArrayRoleDefinitionIdUnique) -contains ($customRoleAll.Id)) { + $roleIsUsed = $true + } + } + } + + #role used in a policyDef (rule roledefinitionIds) + if ($htRoleDefinitionIdsUsedInPolicy.Keys -contains "/providers/Microsoft.Authorization/roleDefinitions/$($customRoleAll.Id)") { + $roleIsUsed = $true + } + + if ($roleIsUsed -eq $false) { + $null = $arrayCustomRolesOrphanedFinalIncludingResourceGroups.Add($customRoleAll) + } + } + } + + if (($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customRolesOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYOrphanedCustomRoles = $null + $htmlSUMMARYOrphanedCustomRoles = foreach ($customRoleOrphaned in $arrayCustomRolesOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.Name } }) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYOrphanedCustomRoles) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdAssignable Scopes
$($customRoleOrphaned.Name -replace '<', '<' -replace '>', '>')$($customRoleOrphaned.Id)$(($customRoleOrphaned.AssignableScopes).count) ($($customRoleOrphaned.AssignableScopes -join "$CsvDelimiterOpposite "))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count) Orphaned Custom Role definitions ($scopeNamingSummary)

+"@) + } + #not renant root + } + else { + $mgs = (($optimizedTableForPathQueryMg.where( { $_.mgId -ne '' -and $_.Level -ne '0' })) | Select-Object MgId -Unique) + $arrayCustomRolesOrphanedFinalIncludingResourceGroups = [System.Collections.ArrayList]@() + + $mgSubRoleAssignmentsArrayRoleDefinitionIdUnique = $mgSubRoleAssignmentsArrayFromHTValues.RoleDefinitionId | Sort-Object -Unique + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + $rgResRoleAssignmentsArrayRoleDefinitionIdUnique = $rgResRoleAssignmentsArrayFromHTValues.RoleDefinitionId | Sort-Object -Unique + } + if (($tenantCustomRoles).count -gt 0) { + foreach ($customRoleAll in $tenantCustomRoles) { + $roleIsUsed = $false + $customRoleAssignableScopes = $customRoleAll.AssignableScopes + foreach ($customRoleAssignableScope in $customRoleAssignableScopes) { + if (($customRoleAssignableScope) -like '/providers/Microsoft.Management/managementGroups/*') { + $roleAssignableScopeMg = $customRoleAssignableScope -replace '/providers/Microsoft.Management/managementGroups/', '' + if ($mgs.MgId -notcontains ($roleAssignableScopeMg)) { + #assignableScope outside of the ManagementGroupId Scope + $roleIsUsed = $true + Continue + } + } + } + if ($roleIsUsed -eq $false) { + if (($mgSubRoleAssignmentsArrayRoleDefinitionIdUnique) -contains ($customRoleAll.Id)) { + $roleIsUsed = $true + } + } + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if ($roleIsUsed -eq $false) { + if (($rgResRoleAssignmentsArrayRoleDefinitionIdUnique) -contains ($customRoleAll.Id)) { + $roleIsUsed = $true + } + } + } + + #role used in a policyDef (rule roledefinitionIds) + if ($htRoleDefinitionIdsUsedInPolicy.Keys -contains "/providers/Microsoft.Authorization/roleDefinitions/$($customRoleAll.Id)") { + $roleIsUsed = $true + } + + if ($roleIsUsed -eq $false) { + $null = $arrayCustomRolesOrphanedFinalIncludingResourceGroups.Add($customRoleAll) + } + } + } + + if (($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count -gt 0) { + $tfCount = ($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count + $htmlTableId = 'TenantSummary_customRolesOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYOrphanedCustomRoles = $null + $htmlSUMMARYOrphanedCustomRoles = foreach ($inScopeCustomRole in $arrayCustomRolesOrphanedFinalIncludingResourceGroups | Sort-Object @{Expression = { $_.Name } }) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYOrphanedCustomRoles) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole Assignable Scopes
$($inScopeCustomRole.Name -replace '<', '<' -replace '>', '>')$($inScopeCustomRole.Id)$(($inScopeCustomRole.AssignableScopes).count) ($($inScopeCustomRole.AssignableScopes -join "$CsvDelimiterOpposite "))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($arrayCustomRolesOrphanedFinalIncludingResourceGroups).count) Orphaned Custom Role definitions ($scopeNamingSummary)

+"@) + } + } + $endSUMMARYOrphanedCustomRoles = Get-Date + Write-Host " SUMMARYOrphanedCustomRoles duration: $((New-TimeSpan -Start $startSUMMARYOrphanedCustomRoles -End $endSUMMARYOrphanedCustomRoles).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedCustomRoles -End $endSUMMARYOrphanedCustomRoles).TotalSeconds) seconds)" + + #endregion SUMMARYOrphanedCustomRoles + + #region SUMMARYOrphanedRoleAssignments + Write-Host ' processing TenantSummary RoleAssignments orphaned' + $roleAssignmentsOrphanedAll = ($rbacBaseQuery.where( { $_.RoleAssignmentIdentityObjectType -eq 'Unknown' })) | Sort-Object -Property RoleAssignmentId + $roleAssignmentsOrphanedUnique = $roleAssignmentsOrphanedAll | Sort-Object -Property RoleAssignmentId -Unique + + if (($roleAssignmentsOrphanedUnique).count -gt 0) { + $tfCount = ($roleAssignmentsOrphanedUnique).count + $htmlTableId = 'TenantSummary_roleAssignmentsOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYOrphanedRoleAssignments = $null + foreach ($roleAssignmentOrphanedUnique in $roleAssignmentsOrphanedUnique) { + $hlpRoleAssignmentsAll = $roleAssignmentsOrphanedAll.where( { $_.RoleAssignmentId -eq $roleAssignmentOrphanedUnique.RoleAssignmentId }) + $impactedMgs = $hlpRoleAssignmentsAll.where( { [String]::IsNullOrEmpty($_.SubscriptionId) }) | Sort-Object -Property MgId + $impactedSubs = $hlpRoleAssignmentsAll.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) }) | Sort-Object -Property SubscriptionId + $htmlSUMMARYOrphanedRoleAssignments += @" + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYOrphanedRoleAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
Role AssignmentIdRole NameRoleIdImpacted Mg/Sub
$($roleAssignmentOrphanedUnique.RoleAssignmentId)$($roleAssignmentOrphanedUnique.RoleDefinitionName -replace '<', '<' -replace '>', '>')$($roleAssignmentOrphanedUnique.RoleDefinitionId)Mg: $(($impactedMgs).count); Sub: $(($impactedSubs).count)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($roleAssignmentsOrphanedUnique).count) Orphaned Role assignments ($scopeNamingSummary)

+"@) + } + #endregion SUMMARYOrphanedRoleAssignments + + #region SUMMARYClassicAdministrators + Write-Host ' processing TenantSummary ClassicAdministrators' + + if ($htClassicAdministrators.Keys.Count -gt 0) { + $tfCount = $htClassicAdministrators.Values.ClassicAdministrators.Count + $htmlTableId = 'TenantSummary_ClassicAdministrators' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYClassicAdministrators = $null + $classicAdministrators = $htClassicAdministrators.Values.ClassicAdministrators | Sort-Object -Property Subscription, Role, Identity + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_ClassicAdministrators" + Write-Host " Exporting ClassicAdministrators CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $classicAdministrators | Select-Object -ExcludeProperty Id | Sort-Object -Property Subscription, SubscriptionId, Role | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + $htmlSUMMARYClassicAdministrators = foreach ($classicAdministrator in $classicAdministrators) { + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYClassicAdministrators) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdMgPathRoleIdentity
$($classicAdministrator.Subscription)$($classicAdministrator.SubscriptionId)$($classicAdministrator.SubscriptionMgPath)$($classicAdministrator.Role)$($classicAdministrator.Identity)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No ClassicAdministrators

+'@) + } + #endregion SUMMARYClassicAdministrators + + #region SUMMARYRoleAssignmentsAll + $startRoleAssignmentsAll = Get-Date + Write-Host ' processing TenantSummary RoleAssignments' + + $startCreateRBACAllHTMLbeforeForeach = Get-Date + + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].RBACAtScopeOnly) { + $rbacAllAtScope = ($rbacAll.where( { ((-not [string]::IsNullOrEmpty($_.SubscriptionId) -and $_.scope -notlike 'inherited *')) -or ([string]::IsNullOrEmpty($_.SubscriptionId)) })) + $rbacAllCount = $rbacAllAtScope.Count + $rbacAllUniqueCount = ($rbacAllAtScope.where({ $_.roleAssignmentId }).RoleAssignmentId | Sort-Object -Unique).count + } + else { + $rbacAllCount = $rbacAll.Count + $rbacAllUniqueCount = ($rbacAll.where({ $_.roleAssignmentId }).RoleAssignmentId | Sort-Object -Unique).count + } + + if ($rbacAllCount -gt 0) { + $uniqueRoleAssignmentsCount = ($rbacAll.RoleAssignmentId | Sort-Object -Unique).count + $tfCount = $rbacAllCount + + if (-not $NoCsvExport) { + $startCreateRBACAllCSV = Get-Date + + $csvFilename = "$($filename)_RoleAssignments" + Write-Host " Exporting RoleAssignments CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + if ($CsvExportUseQuotesAsNeeded) { + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].RBACAtScopeOnly) { + $rbacAllAtScope | Sort-Object -Property Level, RoleAssignmentId, MgId, SubscriptionId, RoleClear, ObjectId | Select-Object -ExcludeProperty Role, RbacRelatedPolicyAssignment | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded + } + else { + $rbacAll | Sort-Object -Property Level, RoleAssignmentId, MgId, SubscriptionId, RoleClear, ObjectId | Select-Object -ExcludeProperty Role, RbacRelatedPolicyAssignment | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation -UseQuotes AsNeeded + } + } + else { + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].RBACAtScopeOnly) { + $rbacAllAtScope | Sort-Object -Property Level, RoleAssignmentId, MgId, SubscriptionId, RoleClear, ObjectId | Select-Object -ExcludeProperty Role, RbacRelatedPolicyAssignment | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + else { + $rbacAll | Sort-Object -Property Level, RoleAssignmentId, MgId, SubscriptionId, RoleClear, ObjectId | Select-Object -ExcludeProperty Role, RbacRelatedPolicyAssignment | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + + $endCreateRBACAllCSV = Get-Date + Write-Host " CreateRBACAll CSV duration: $((New-TimeSpan -Start $startCreateRBACAllCSV -End $endCreateRBACAllCSV).TotalMinutes) minutes ($((New-TimeSpan -Start $startCreateRBACAllCSV -End $endCreateRBACAllCSV).TotalSeconds) seconds)" + } + + if ($tfCount -gt $HtmlTableRowsLimit) { + Write-Host " !Skipping TenantSummary RoleAssignments HTML processing as $tfCount lines is exceeding the critical rows limit of $HtmlTableRowsLimit" -ForegroundColor Yellow + [void]$htmlTenantSummary.AppendLine(@" + +
+ Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
+ You can adjust the html row limit by using parameter -HtmlTableRowsLimit
+ You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
+ Check the parameters documentation Azure Governance Visualizer docs +
+"@) + } + else { + + $roleAssignmentsInfo = @() + #all + $roleAssignmentsInfo += "All: $($rbacAllUniqueCount)" + #static + $roleAssignmentsInfo += "Standing: $((($rbacAll.where({ $_.RoleAssignmentPIMRelated -eq $false })).roleAssignmentId | Sort-Object -Unique).count)" + #PIM + foreach ($pimAssignmentInfo in ($rbacAll.where({ $_.RoleAssignmentPIMRelated -and $_.Scope -notlike 'inherited*' })) | Group-Object -Property RoleAssignmentPIMAssignmentType) { + $roleAssignmentsInfo += "PIM-$($pimAssignmentInfo.Name): $($pimAssignmentInfo.Count)" + } + + $htmlTableId = 'TenantSummary_roleAssignmentsAll' + $noteOrNot = '' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma
+*Depending on the number of rows and your computer´s performance the table may respond with delay, download the csv for better filtering experience +"@) + + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + $cnter = 0 + $roleAssignmentsAllCount = $rbacAllCount + $htmlSummaryRoleAssignmentsAll = $null + $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlTenantSummary = [System.Text.StringBuilder]::new() + + $endCreateRBACAllHTMLbeforeForeach = Get-Date + Write-Host " CreateRBACAll HTML before Foreach duration: $((New-TimeSpan -Start $startCreateRBACAllHTMLbeforeForeach -End $endCreateRBACAllHTMLbeforeForeach).TotalMinutes) minutes ($((New-TimeSpan -Start $startCreateRBACAllHTMLbeforeForeach -End $endCreateRBACAllHTMLbeforeForeach).TotalSeconds) seconds)" + + $startSortRBACAll = Get-Date + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].RBACAtScopeOnly) { + $rbacAllSorted = $rbacAllAtScope | Sort-Object -Property Level, MgName, MgId, SubscriptionName, SubscriptionId, Scope, Role, RoleId, ObjectId, RoleAssignmentId + } + else { + $rbacAllSorted = $rbacAll | Sort-Object -Property Level, MgName, MgId, SubscriptionName, SubscriptionId, Scope, Role, RoleId, ObjectId, RoleAssignmentId + } + + $endSortRBACAll = Get-Date + Write-Host " Sort RBACAll duration: $((New-TimeSpan -Start $startSortRBACAll -End $endSortRBACAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startSortRBACAll -End $endSortRBACAll).TotalSeconds) seconds)" + + $startCreateRBACAllHTMLForeach = Get-Date + $htmlSummaryRoleAssignmentsAll = [System.Text.StringBuilder]::new() + foreach ($roleAssignment in $rbacAllSorted) { + $cnter++ + if ($cnter % 1000 -eq 0) { + Write-Host " create HTML $cnter of $rbacAllCount RoleAssignments processed" + if ($cnter % 5000 -eq 0) { + Write-Host ' appending..' + $htmlSummaryRoleAssignmentsAll | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlSummaryRoleAssignmentsAll = [System.Text.StringBuilder]::new() + } + } + + if ($roleAssignment.RoleType -eq 'Custom') { + $roleName = ($roleAssignment.Role -replace '<', '<' -replace '>', '>') + } + else { + $roleName = $roleAssignment.Role + } + + [void]$htmlSummaryRoleAssignmentsAll.AppendFormat( + @' + + + + + + + + + + + + + + + + + + + + + + + + + + + + +'@, $roleAssignment.ScopeTenOrMgOrSubOrRGOrRes, + $roleAssignment.MgId, + ($roleAssignment.MgName -replace '<', '<' -replace '>', '>'), + $roleAssignment.SubscriptionId, + $roleAssignment.SubscriptionName, + $roleAssignment.Scope, + $roleName, + $roleAssignment.RoleId, + $roleAssignment.RoleType, + $roleAssignment.RoleDataRelated, + $roleAssignment.RoleCanDoRoleAssignments, + $roleAssignment.ObjectDisplayName, + $roleAssignment.ObjectSignInName, + $roleAssignment.ObjectId, + $roleAssignment.ObjectType, + $roleAssignment.AssignmentType, + $roleAssignment.AssignmentInheritFrom, + $roleAssignment.GroupMembersCount, + $roleAssignment.RoleAssignmentPIMRelated, + $roleAssignment.RoleAssignmentPIMAssignmentType, + $roleAssignment.RoleAssignmentPIMAssignmentSlotStart, + $roleAssignment.RoleAssignmentPIMAssignmentSlotEnd, + $roleAssignment.RoleAssignmentId, + ($roleAssignment.RbacRelatedPolicyAssignment), + $roleAssignment.CreatedOn, + $roleAssignment.CreatedBy + ) + + } + $start = Get-Date + [void]$htmlTenantSummary.AppendLine($htmlSummaryRoleAssignmentsAll) + + $htmlTenantSummary | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $htmlSummaryRoleAssignmentsAll = $null #cleanup + $htmlTenantSummary = [System.Text.StringBuilder]::new() + $end = Get-Date + + $endCreateRBACAllHTMLForeach = Get-Date + Write-Host " CreateRBACAll HTML Foreach duration: $((New-TimeSpan -Start $startCreateRBACAllHTMLForeach -End $endCreateRBACAllHTMLForeach).TotalMinutes) minutes ($((New-TimeSpan -Start $startCreateRBACAllHTMLForeach -End $endCreateRBACAllHTMLForeach).TotalSeconds) seconds)" + + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeManagement Group IdManagement Group NameSubscriptionIdSubscription NameAssignment ScopeRoleRole IdRole TypeDataCan do Role assignmentIdentity DisplaynameIdentity SignInNameIdentity ObjectIdIdentity TypeApplicabilityApplies through membership Group DetailsPIMPIM assignment typePIM startPIM endRole AssignmentIdRelated Policy Assignment $noteOrNotCreatedOnCreatedBy
{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}{20}{21}{22}{23}{24}{25}
+
+ +"@) + + } + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($rbacAllCount) Role assignments

+"@) + } + + $endRoleAssignmentsAll = Get-Date + Write-Host " SummaryRoleAssignmentsAll duration: $((New-TimeSpan -Start $startRoleAssignmentsAll -End $endRoleAssignmentsAll).TotalMinutes) minutes ($((New-TimeSpan -Start $startRoleAssignmentsAll -End $endRoleAssignmentsAll).TotalSeconds) seconds)" + #endregion SUMMARYRoleAssignmentsAll + + #region SUMMARYPIMEligibility + if (-not $NoPIMEligibility) { + $startPIMEligibility = Get-Date + Write-Host ' processing TenantSummary PIMEligibility' + + if ($arrayPIMEligible.Count -gt 0) { + $tfCount = $arrayPIMEligible.Count + $htmlTableId = 'TenantSummary_PIMEligibility' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYPIMEligibility = $null + $PIMEligibleEnrichedSorted = $PIMEligibleEnriched | Sort-Object -Property Scope, MgLevel, ScopeName, IdentityDisplayName, PIMEligibilityId + $tfCountCnt = $PIMEligibleEnrichedSorted.Count + $htmlSUMMARYPIMEligibility = foreach ($PIMEligible in $PIMEligibleEnrichedSorted) { + @" + + + + + + + + + + + + + + + + + + + + + +"@ + } + + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_PIMEligibility" + Write-Host " Exporting PIMEligibility CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $PIMEligibleEnrichedSorted | Select-Object -ExcludeProperty RoleClear | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPIMEligibility) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScopeIdScopeNameMgPathMgLevelRoleRole IdRole typeIdentity ObjectIdIdentity DisplayNameIdentity SignInNameIdentity TypeIdentity ApplicabilityApplies through (AAD Grp)PIM EligibilityPIM Eligibility inhherted (MG)PIM startPIM endPIM Eligibility Id
$($PIMEligible.Scope)$($PIMEligible.ScopeId)$($PIMEligible.ScopeName)$($PIMEligible.MgPath -join '/')$($PIMEligible.MgLevel)$($PIMEligible.Role)$($PIMEligible.RoleIdGuid)$($PIMEligible.RoleType)$($PIMEligible.IdentityObjectId)$($PIMEligible.IdentityDisplayName)$($PIMEligible.IdentitySignInName)$($PIMEligible.IdentityType)$($PIMEligible.IdentityApplicability)$($PIMEligible.AppliesThrough)$($PIMEligible.PIMEligibility)$($PIMEligible.PIMEligibilityInheritedFrom)$($PIMEligible.PIMEligibilityStartDateTime)$($PIMEligible.PIMEligibilityEndDateTime)$($PIMEligible.PIMEligibilityId)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No PIM Eligibility

+'@) + } + + $endPIMEligibility = Get-Date + Write-Host " TenantSummary PIMEligibility duration: $((New-TimeSpan -Start $startPIMEligibility -End $endPIMEligibility).TotalMinutes) minutes ($((New-TimeSpan -Start $startPIMEligibility -End $endPIMEligibility).TotalSeconds) seconds)" + } + else { + if ($azAPICallConf['htParameters'].accountType -ne 'User' -and $NoPIMEligibility) { + [void]$htmlTenantSummary.AppendLine(@" +

No PIM Eligibility - parameter -NoPIMEligibility = $NoPIMEligibility

+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No PIM Eligibility - run Azure Governance Visualizer with a Service Principal to get PIM Eligibility insights

+'@) + } + } + #endregion SUMMARYPIMEligibility + + #region SUMMARYSecurityCustomRoles + Write-Host ' processing TenantSummary Custom Roles security (owner permissions)' + $customRolesOwnerAll = ($rbacBaseQuery.where( { $_.RoleSecurityCustomRoleOwner -eq 1 })) | Sort-Object -Property RoleDefinitionId + $customRolesOwnerHtAll = $tenantCustomRoles.where( { $_.Actions -eq '*' -and ($_.NotActions).length -eq 0 }) + if (($customRolesOwnerHtAll).count -gt 0) { + $tfCount = ($customRolesOwnerHtAll).count + $htmlTableId = 'TenantSummary_CustomRoleOwner' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYSecurityCustomRoles = $null + foreach ($customRole in ($customRolesOwnerHtAll | Sort-Object -Property Name, Id)) { + $customRoleOwnersAllAssignmentsCount = ((($customRolesOwnerAll.where( { $_.RoleDefinitionId -eq $customRole.Id })).RoleAssignmentId | Sort-Object -Unique)).count + if ($customRoleOwnersAllAssignmentsCount -gt 0) { + $customRoleRoleAssignmentsArray = [System.Collections.ArrayList]@() + $customRoleRoleAssignmentIds = ($customRolesOwnerAll.where( { $_.RoleDefinitionId -eq $customRole.Id })).RoleAssignmentId | Sort-Object -Unique + foreach ($customRoleRoleAssignmentId in $customRoleRoleAssignmentIds) { + $null = $customRoleRoleAssignmentsArray.Add($customRoleRoleAssignmentId) + } + $customRoleRoleAssignmentsOutput = "$customRoleOwnersAllAssignmentsCount ($($customRoleRoleAssignmentsArray -join "$CsvDelimiterOpposite "))" + } + else { + $customRoleRoleAssignmentsOutput = "$customRoleOwnersAllAssignmentsCount" + } + $htmlSUMMARYSecurityCustomRoles += @" + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityCustomRoles) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole assignmentsAssignable Scopes
$($customRole.Name -replace '<', '<' -replace '>', '>')$($customRole.Id)$($customRoleRoleAssignmentsOutput)$(($customRole.AssignableScopes).count) ($($customRole.AssignableScopes -join "$CsvDelimiterOpposite "))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($customRolesOwnerHtAll).count) Custom Role definitions Owner permissions ($scopeNamingSummary)

+"@) + } + #endregion SUMMARYSecurityCustomRoles + + #region SUMMARYSecurityRolesCanDoRoleAssignments + Write-Host ' processing TenantSummary Roles security (can apply Role assignments)' + if ($tenantAllRolesCanDoRoleAssignmentsCount -gt 0) { + + #$roleAssignments4RolesCanDoRoleAssignments = (($rbacBaseQuery.where( { $_.RoleCanDoRoleAssignments -eq $true })) | Sort-Object -Property RoleAssignmentId -Unique) | Select-Object RoleAssignmentId, RoleDefinitionId + $roleAssignments4RolesCanDoRoleAssignments = (($rbacBaseQuery | Sort-Object -Property RoleAssignmentId -Unique).where( { $_.RoleCanDoRoleAssignments -eq $true })) | Select-Object RoleAssignmentId, RoleDefinitionId + $htRoleAssignments4RolesCanDoRoleAssignments = @{} + foreach ($roleAssignment4RolesCanDoRoleAssignments in $roleAssignments4RolesCanDoRoleAssignments) { + if (-not $htRoleAssignments4RolesCanDoRoleAssignments.($roleAssignment4RolesCanDoRoleAssignments.RoleDefinitionId)) { + $htRoleAssignments4RolesCanDoRoleAssignments.($roleAssignment4RolesCanDoRoleAssignments.RoleDefinitionId) = @{} + $htRoleAssignments4RolesCanDoRoleAssignments.($roleAssignment4RolesCanDoRoleAssignments.RoleDefinitionId).roleAssignments = [System.Collections.ArrayList]@() + } + $null = $htRoleAssignments4RolesCanDoRoleAssignments.($roleAssignment4RolesCanDoRoleAssignments.RoleDefinitionId).roleAssignments.Add($roleAssignment4RolesCanDoRoleAssignments.RoleAssignmentId) + } + + $tfCount = $tenantAllRolesCanDoRoleAssignmentsCount + $htmlTableId = 'TenantSummary_RolesCanDoRoleAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYSecurityRolesCanDoRoleAssignments = $null + foreach ($role in ($tenantAllRolesCanDoRoleAssignments | Sort-Object -Property Name)) { + if ($role.IsCustom) { + $roleType = 'Custom' + $roleAssignableScopes = "$(($role.AssignableScopes).count) ($($role.AssignableScopes -join "$CsvDelimiterOpposite "))" + } + else { + $roleType = 'BuiltIn' + $roleAssignableScopes = '' + } + + if ($htRoleAssignments4RolesCanDoRoleAssignments.($role.Id).roleAssignments.Count -gt 0) { + $roleAssignments = "$($htRoleAssignments4RolesCanDoRoleAssignments.($role.Id).roleAssignments.Count) ($($htRoleAssignments4RolesCanDoRoleAssignments.($role.Id).roleAssignments -join ', '))" + } + else { + $roleAssignments = 0 + } + + $htmlSUMMARYSecurityRolesCanDoRoleAssignments += @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityRolesCanDoRoleAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdTypeRole assignmentsAssignable Scopes
$($role.Name -replace '<', '<' -replace '>', '>')$($role.Id)$($roleType)$($roleAssignments)$($roleAssignableScopes)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($tenantAllRolesCanDoRoleAssignmentsCount) Role definitions can apply Role assignments

+"@) + } + #endregion SUMMARYSecurityRolesCanDoRoleAssignments + + #region SUMMARYSecurityOwnerAssignmentSP + $startSUMMARYSecurityOwnerAssignmentSP = Get-Date + Write-Host ' processing TenantSummary RoleAssignments security (owner SP)' + $roleAssignmentsOwnerAssignmentSPAll = ($rbacBaseQuery.where( { $_.RoleSecurityOwnerAssignmentSP -eq 1 })) | Sort-Object -Property RoleAssignmentId + $roleAssignmentsOwnerAssignmentSP = $roleAssignmentsOwnerAssignmentSPAll | Sort-Object -Property RoleAssignmentId -Unique + if (($roleAssignmentsOwnerAssignmentSP).count -gt 0) { + $tfCount = ($roleAssignmentsOwnerAssignmentSP).count + $htmlTableId = 'TenantSummary_roleAssignmentsOwnerAssignmentSP' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYSecurityOwnerAssignmentSP = $null + $htmlSUMMARYSecurityOwnerAssignmentSP = foreach ($roleAssignmentOwnerAssignmentSP in ($roleAssignmentsOwnerAssignmentSP)) { + $hlpRoleAssignmentsAll = $roleAssignmentsOwnerAssignmentSPAll.where( { $_.RoleAssignmentId -eq $roleAssignmentOwnerAssignmentSP.RoleAssignmentId }) + $impactedMgs = $hlpRoleAssignmentsAll.where( { [String]::IsNullOrEmpty($_.SubscriptionId) }) + $impactedSubs = $hlpRoleAssignmentsAll.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) }) + $servicePrincipal = $roleAssignmentsOwnerAssignmentSP.where( { $_.RoleAssignmentId -eq $roleAssignmentOwnerAssignmentSP.RoleAssignmentId }) | Get-Unique + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityOwnerAssignmentSP) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole AssignmentServicePrincipal (ObjId)Impacted Mg/Sub
$($roleAssignmentOwnerAssignmentSP.RoleDefinitionName -replace '<', '<' -replace '>', '>')$($roleAssignmentOwnerAssignmentSP.RoleDefinitionId)$($roleAssignmentOwnerAssignmentSP.RoleAssignmentId)$($servicePrincipal.RoleAssignmentIdentityDisplayname) ($($servicePrincipal.RoleAssignmentIdentityObjectId))Mg: $(($impactedMgs.mgid | Sort-Object -Unique).count); Sub: $(($impactedSubs.subscriptionId | Sort-Object -Unique).count)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($roleAssignmentsOwnerAssignmentSP).count) Owner permission assignments to ServicePrincipal ($scopeNamingSummary)

+"@) + } + $endSUMMARYSecurityOwnerAssignmentSP = Get-Date + Write-Host " TenantSummary RoleAssignments security (owner SP) duration: $((New-TimeSpan -Start $startSUMMARYSecurityOwnerAssignmentSP -End $endSUMMARYSecurityOwnerAssignmentSP).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSecurityOwnerAssignmentSP -End $endSUMMARYSecurityOwnerAssignmentSP).TotalSeconds) seconds)" + #endregion SUMMARYSecurityOwnerAssignmentSP + + #region SUMMARYSecurityOwnerAssignmentNotGroup + Write-Host ' processing TenantSummary RoleAssignments security (owner notGroup)' + $startSUMMARYSecurityOwnerAssignmentNotGroup = Get-Date + + $roleAssignmentsOwnerAssignmentNotGroup = $rbacBaseQueryArrayListNotGroupOwner | Sort-Object -Property RoleAssignmentId -Unique + $roleAssignmentsOwnerAssignmentNotGroupGrouped = ($rbacBaseQueryArrayListNotGroupOwner | Group-Object -Property roleassignmentId) + + if (($roleAssignmentsOwnerAssignmentNotGroup).count -gt 0) { + $tfCount = ($roleAssignmentsOwnerAssignmentNotGroup).count + $htmlTableId = 'TenantSummary_roleAssignmentsOwnerAssignmentNotGroup' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYSecurityOwnerAssignmentNotGroup = $null + $htmlSUMMARYSecurityOwnerAssignmentNotGroup = foreach ($roleAssignmentOwnerAssignmentNotGroup in ($roleAssignmentsOwnerAssignmentNotGroup)) { + $impactedMgSubBaseQuery = $roleAssignmentsOwnerAssignmentNotGroupGrouped.where( { $_.Name -eq $roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentId }) + $impactedMgs = $impactedMgSubBaseQuery.Group.where( { [String]::IsNullOrEmpty($_.SubscriptionId) }) + $impactedSubs = $impactedMgSubBaseQuery.Group.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) }) + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityOwnerAssignmentNotGroup) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole AssignmentObj TypeObj DisplayNameObj SignInNameObjIdImpacted Mg/Sub
$($roleAssignmentOwnerAssignmentNotGroup.RoleDefinitionName -replace '<', '<' -replace '>', '>')$($roleAssignmentOwnerAssignmentNotGroup.RoleDefinitionId)$($roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentId)$($roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentIdentityObjectType)$($roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentIdentityDisplayname)$($roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentIdentitySignInName)$($roleAssignmentOwnerAssignmentNotGroup.RoleAssignmentIdentityObjectId)Mg: $(($impactedMgs.mgid | Sort-Object -Unique).count); Sub: $(($impactedSubs.subscriptionId | Sort-Object -Unique).count)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($roleAssignmentsOwnerAssignmentNotGroup).count) Owner permission assignments to notGroup ($scopeNamingSummary)

+"@) + } + $endSUMMARYSecurityOwnerAssignmentNotGroup = Get-Date + Write-Host " TenantSummary RoleAssignments security (owner notGroup) duration: $((New-TimeSpan -Start $startSUMMARYSecurityOwnerAssignmentNotGroup -End $endSUMMARYSecurityOwnerAssignmentNotGroup).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSecurityOwnerAssignmentNotGroup -End $endSUMMARYSecurityOwnerAssignmentNotGroup).TotalSeconds) seconds)" + #endregion SUMMARYSecurityOwnerAssignmentNotGroup + + #region SUMMARYSecurityUserAccessAdministratorAssignmentNotGroup + $startSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup = Get-Date + Write-Host ' processing TenantSummary RoleAssignments security (userAccessAdministrator notGroup)' + $roleAssignmentsUserAccessAdministratorAssignmentNotGroup = $rbacBaseQueryArrayListNotGroupUserAccessAdministrator | Sort-Object -Property RoleAssignmentId -Unique + $roleAssignmentsUserAccessAdministratorAssignmentNotGroupGrouped = ($rbacBaseQueryArrayListNotGroupUserAccessAdministrator | Group-Object -Property roleassignmentId) + + if (($roleAssignmentsUserAccessAdministratorAssignmentNotGroup).count -gt 0) { + $tfCount = ($roleAssignmentsUserAccessAdministratorAssignmentNotGroup).count + $htmlTableId = 'TenantSummary_roleAssignmentsUserAccessAdministratorAssignmentNotGroup' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup = $null + $htmlSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup = foreach ($roleAssignmentUserAccessAdministratorAssignmentNotGroup in ($roleAssignmentsUserAccessAdministratorAssignmentNotGroup)) { + $impactedMgSubBaseQuery = $roleAssignmentsUserAccessAdministratorAssignmentNotGroupGrouped.where( { $_.Name -eq $roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentId }) + $impactedMgs = $impactedMgSubBaseQuery.Group.where( { [String]::IsNullOrEmpty($_.SubscriptionId) }) + $impactedSubs = $impactedMgSubBaseQuery.Group.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) }) + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole AssignmentObj TypeObj DisplayNameObj SignInNameObjIdImpacted Mg/Sub
$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleDefinitionName -replace '<', '<' -replace '>', '>')$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleDefinitionId)$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentId)$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentIdentityObjectType)$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentIdentityDisplayname)$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentIdentitySignInName)$($roleAssignmentUserAccessAdministratorAssignmentNotGroup.RoleAssignmentIdentityObjectId)Mg: $(($impactedMgs.mgid | Sort-Object -Unique).count); Sub: $(($impactedSubs.subscriptionId | Sort-Object -Unique).count)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($roleAssignmentsUserAccessAdministratorAssignmentNotGroup).count) UserAccessAdministrator permission assignments to notGroup ($scopeNamingSummary)

+"@) + } + $endSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup = Get-Date + Write-Host " TenantSummary RoleAssignments security (userAccessAdministrator notGroup) duration: $((New-TimeSpan -Start $startSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup -End $endSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup -End $endSUMMARYSecurityUserAccessAdministratorAssignmentNotGroup).TotalSeconds) seconds)" + #endregion SUMMARYSecurityUserAccessAdministratorAssignmentNotGroup + + #region SUMMARYSecurityGuestUserHighPriviledgesAssignments + + $startSUMMARYSecurityGuestUserHighPriviledgesAssignments = Get-Date + Write-Host ' processing TenantSummary RoleAssignments security (high privileged Guest User)' + $highPrivilegedGuestUserRoleAssignments = $rbacAll.where( { ($_.RoleId -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' -or $_.RoleId -eq '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9') -and $_.ObjectType -eq 'User Guest' }) | Sort-Object -Property RoleAssignmentId, ObjectId -Unique + $highPrivilegedGuestUserRoleAssignmentsCount = ($highPrivilegedGuestUserRoleAssignments).Count + if ($highPrivilegedGuestUserRoleAssignmentsCount -gt 0) { + $tfCount = $highPrivilegedGuestUserRoleAssignmentsCount + $htmlTableId = 'TenantSummary_SecurityGuestUserHighPriviledgesAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = $null + $htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments = foreach ($highPrivilegedGuestUserRoleAssignment in ($highPrivilegedGuestUserRoleAssignments)) { + if ($highPrivilededGuestUserRoleAssignment.AssignmentType -eq 'indirect') { + $assignmentInfo = "indirect / AAD Group Membership '$($highPrivilededGuestUserRoleAssignment.AssignmentInheritFrom)'" + } + else { + $assignmentInfo = 'direct' + } + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSecurityGuestUserHighPriviledgesAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdRole AssignmentObj TypeObj DisplayNameObj SignInNameObjIdAssignment direct/indirect
$($highPrivilegedGuestUserRoleAssignment.Role <#-replace "<", "<" -replace ">", ">"#>)$($highPrivilegedGuestUserRoleAssignment.RoleId)$($highPrivilegedGuestUserRoleAssignment.RoleAssignmentId)$($highPrivilegedGuestUserRoleAssignment.ObjectType)$($highPrivilegedGuestUserRoleAssignment.ObjectDisplayName)$($highPrivilegedGuestUserRoleAssignment.ObjectSignInName)$($highPrivilegedGuestUserRoleAssignment.ObjectId)$assignmentInfo
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($highPrivilegedGuestUserRoleAssignmentsCount) Guest Users with high permissions ($scopeNamingSummary)

+"@) + } + $endSUMMARYSecurityGuestUserHighPriviledgesAssignments = Get-Date + Write-Host " TenantSummary RoleAssignments security (high privileged Guest User) duration: $((New-TimeSpan -Start $startSUMMARYSecurityGuestUserHighPriviledgesAssignments -End $endSUMMARYSecurityGuestUserHighPriviledgesAssignments).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSecurityGuestUserHighPriviledgesAssignments -End $endSUMMARYSecurityGuestUserHighPriviledgesAssignments).TotalSeconds) seconds)" + #endregion SUMMARYSecurityGuestUserHighPriviledgesAssignments + + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryRBAC + + showMemoryUsage + + #region tenantSummaryBlueprints + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + #region SUMMARYBlueprintDefinitions + Write-Host ' processing TenantSummary Blueprints' + $blueprintDefinitions = ($blueprintBaseQuery | Where-Object { [String]::IsNullOrEmpty($_.BlueprintAssignmentId) }) + $blueprintDefinitionsCount = ($blueprintDefinitions).count + if ($blueprintDefinitionsCount -gt 0) { + $htmlTableId = 'TenantSummary_BlueprintDefinitions' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYBlueprintDefinitions = $null + $htmlSUMMARYBlueprintDefinitions = foreach ($blueprintDefinition in $blueprintDefinitions | Sort-Object -Property BlueprintName, BlueprintDisplayName) { + @" + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYBlueprintDefinitions) + [void]$htmlTenantSummary.AppendLine(@" + +
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintId
$($blueprintDefinition.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$blueprintDefinitionsCount Blueprint definitions

+"@) + } + #endregion SUMMARYBlueprintDefinitions + + #region SUMMARYBlueprintAssignments + Write-Host ' processing TenantSummary BlueprintAssignments' + $blueprintAssignments = ($blueprintBaseQuery | Where-Object { -not [String]::IsNullOrEmpty($_.BlueprintAssignmentId) }) + $blueprintAssignmentsCount = ($blueprintAssignments).count + + if ($blueprintAssignmentsCount -gt 0) { + $htmlTableId = 'TenantSummary_BlueprintAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlSUMMARYBlueprintAssignments = $null + $htmlSUMMARYBlueprintAssignments = foreach ($blueprintAssignment in $blueprintAssignments | Sort-Object -Property level, BlueprintAssignmentId) { + @" + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYBlueprintAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintIdBlueprint VersionBlueprint AssignmentId
$($blueprintAssignment.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintAssignment.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintAssignment.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintAssignment.BlueprintId -replace '<', '<' -replace '>', '>')$($blueprintAssignment.BlueprintAssignmentVersion)$($blueprintAssignment.BlueprintAssignmentId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$blueprintAssignmentsCount Blueprint assignments

+"@) + } + #endregion SUMMARYBlueprintAssignments + + #region SUMMARYBlueprintsOrphaned + Write-Host ' processing TenantSummary Blueprint definitions orphaned' + $blueprintDefinitionsOrphanedArray = @() + if ($blueprintDefinitionsCount -gt 0) { + if ($blueprintAssignmentsCount -gt 0) { + $blueprintDefinitionsOrphanedArray += foreach ($blueprintDefinition in $blueprintDefinitions) { + if (($blueprintAssignments.BlueprintId) -notcontains ($blueprintDefinition.BlueprintId)) { + $blueprintDefinition + } + } + } + else { + $blueprintDefinitionsOrphanedArray += foreach ($blueprintDefinition in $blueprintDefinitions) { + $blueprintDefinition + } + } + } + $blueprintDefinitionsOrphanedCount = ($blueprintDefinitionsOrphanedArray).count + + if ($blueprintDefinitionsOrphanedCount -gt 0) { + + $htmlTableId = 'TenantSummary_BlueprintsOrphaned' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYBlueprintsOrphaned = $null + $htmlSUMMARYBlueprintsOrphaned = foreach ($blueprintDefinition in $blueprintDefinitionsOrphanedArray | Sort-Object -Property BlueprintId) { + @" + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYBlueprintsOrphaned) + [void]$htmlTenantSummary.AppendLine(@" + +
Blueprint NameBlueprint DisplayNameBlueprint DescriptionBlueprintId
$($blueprintDefinition.BlueprintName -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintDisplayName -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintDescription -replace '<', '<' -replace '>', '>')$($blueprintDefinition.BlueprintId -replace '<', '<' -replace '>', '>')
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$blueprintDefinitionsOrphanedCount Orphaned Blueprint definitions

+"@) + } + #endregion SUMMARYBlueprintsOrphaned + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryBlueprints + + showMemoryUsage + + #region tenantSummaryManagementGroups + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + #region SUMMARYMGs + $startSUMMARYMGs = Get-Date + Write-Host ' processing TenantSummary ManagementGroups' + + $summaryManagementGroups = $optimizedTableForPathQueryMg | Sort-Object -Property Level, mgid, mgParentId + $summaryManagementGroupsCount = ($summaryManagementGroups).Count + if ($summaryManagementGroupsCount -gt 0) { + $tfCount = $summaryManagementGroupsCount + $htmlTableId = 'TenantSummary_ManagementGroups' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { + [void]$htmlTenantSummary.AppendLine(@' + +'@) + } + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + [void]$htmlTenantSummary.AppendLine(@' + + + + +'@) + $htmlSUMMARYManagementGroups = $null + $cnter = 0 + $htmlSUMMARYManagementGroups = foreach ($summaryManagementGroup in $summaryManagementGroups) { + + $mgPath = $htManagementGroupsMgPath.($summaryManagementGroup.mgId).pathDelimited + + if ($summaryManagementGroup.mgid -eq $mgSubPathTopMg -and ($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + $pathhlper = "$($mgPath)" + $arrayTotalCostSummaryMgSummary = 'n/a' + $mgAllChildMgsCountTotal = 'n/a' + $mgAllChildMgsCountDirect = 'n/a' + $mgAllChildSubscriptionsCountTotal = 'n/a' + $mgAllChildSubscriptionsCountDirect = 'n/a' + $mgSecureScore = 'n/a' + } + else { + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($allConsumptionDataCount -gt 0) { + $arrayTotalCostSummaryMgSummary = @() + if ($htManagementGroupsCost.($summaryManagementGroup.mgid)) { + foreach ($currency in $htManagementGroupsCost.($summaryManagementGroup.mgid).currencies) { + $hlper = $htManagementGroupsCost.($summaryManagementGroup.mgid) + $totalCost = $hlper."mgTotalCost_$($currency)" + if ([math]::Round($totalCost, 2) -eq 0) { + $totalCost = $totalCost.ToString('0.0000') + } + else { + $totalCost = [math]::Round($totalCost, 2).ToString('0.00') + } + $totalCostGeneratedByResourceTypes = ($hlper."resourceTypesThatGeneratedCost_$($currency)").Count + $totalCostGeneratedByResources = $hlper."resourcesThatGeneratedCost_$($currency)" + $totalCostGeneratedBySubscriptions = $hlper."subscriptionsThatGeneratedCost_$($currency)" + $arrayTotalCostSummaryMgSummary += "$($totalCost) $($currency) generated by $($totalCostGeneratedByResources) Resources ($($totalCostGeneratedByResourceTypes) ResourceTypes) in $($totalCostGeneratedBySubscriptions) Subscriptions" + } + } + else { + $arrayTotalCostSummaryMgSummary = 'no consumption data available' + } + } + else { + $arrayTotalCostSummaryMgSummary = 'no consumption data available' + } + } + $pathhlper = " $($mgPath)" + + #childrenMgInfo + $mgAllChildMgs = [System.Collections.ArrayList]@() + foreach ($entry in $htManagementGroupsMgPath.keys) { + if (($htManagementGroupsMgPath.($entry).path) -contains $($summaryManagementGroup.mgid)) { + $null = $mgAllChildMgs.Add($entry) + } + } + $mgAllChildMgsCountTotal = (($mgAllChildMgs).Count - 1) + $mgAllChildMgsCountDirect = $htMgDetails.($summaryManagementGroup.mgid).mgChildrenCount + + $mgAllChildSubscriptions = [System.Collections.ArrayList]@() + $mgDirectChildSubscriptions = [System.Collections.ArrayList]@() + foreach ($entry in $htSubscriptionsMgPath.keys) { + if (($htSubscriptionsMgPath.($entry).path) -contains $($summaryManagementGroup.mgid)) { + $null = $mgAllChildSubscriptions.Add($entry) + } + if (($htSubscriptionsMgPath.($entry).parent) -eq $($summaryManagementGroup.mgid)) { + $null = $mgDirectChildSubscriptions.Add($entry) + } + } + + $mgAllChildSubscriptionsCountTotal = (($mgAllChildSubscriptions).Count) + $mgAllChildSubscriptionsCountDirect = (($mgDirectChildSubscriptions).Count) + + if ($htMgASCSecureScore.($summaryManagementGroup.mgId).SecureScore) { + if ([string]::IsNullOrEmpty($htMgASCSecureScore.($summaryManagementGroup.mgId).SecureScore) -or [string]::IsNullOrWhiteSpace($htMgASCSecureScore.($summaryManagementGroup.mgId).SecureScore)) { + $mgSecureScore = 'n/a' + } + else { + $mgSecureScore = $htMgASCSecureScore.($summaryManagementGroup.mgId).SecureScore + } + } + else { + $mgSecureScore = 'n/a' + } + } + + @" + + + + + + + + +"@ + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { + @" + +"@ + } + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + @" + +"@ + } + @" + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYManagementGroups) + [void]$htmlTenantSummary.AppendLine(@" + +
LevelManagementGroupManagementGroup IdMg children (total)Mg children (direct)Sub children (total)Sub children (direct)MG MDfC ScoreCost ($($AzureConsumptionPeriod)d)Path
$($summaryManagementGroup.level)$($summaryManagementGroup.mgName -replace '<', '<' -replace '>', '>')$($summaryManagementGroup.mgId)$($mgAllChildMgsCountTotal)$($mgAllChildMgsCountDirect)$($mgAllChildSubscriptionsCountTotal)$($mgAllChildSubscriptionsCountDirect)$($mgSecureScore)$($arrayTotalCostSummaryMgSummary -join ', ')$($pathhlper)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($summaryManagementGroupsCount) Management Groups

+"@) + } + $endSUMMARYMGs = Get-Date + Write-Host " SUMMARYMGs duration: $((New-TimeSpan -Start $startSUMMARYMGs -End $endSUMMARYMGs).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYMGs -End $endSUMMARYMGs).TotalSeconds) seconds)" + #endregion SUMMARYMGs + + #region SUMMARYMGdefault + Write-Host ' processing TenantSummary ManagementGroups - default Management Group' + [void]$htmlTenantSummary.AppendLine(@" +

Hierarchy Settings | Default Management Group Id: '$($defaultManagementGroupId)' docs

+"@) + #endregion SUMMARYMGdefault + + #region SUMMARYMGRequireAuthorizationForGroupCreation + Write-Host ' processing TenantSummary ManagementGroups - requireAuthorizationForGroupCreation Management Group' + [void]$htmlTenantSummary.AppendLine(@" +

Hierarchy Settings | Require authorization for Management Group creation: '$($requireAuthorizationForGroupCreation)' docs

+"@) + #endregion SUMMARYMGRequireAuthorizationForGroupCreation + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryManagementGroups + + showMemoryUsage + + #region tenantSummarySubscriptionsResourceDefenderPSRule + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + #region SUMMARYSubs + $startSUMMARYSubs = Get-Date + Write-Host ' processing TenantSummary Subscriptions' + $summarySubscriptions = $optimizedTableForPathQueryMgAndSub | Sort-Object -Property Subscription + $summarySubscriptionsCount = ($summarySubscriptions).Count + + $arrayPIMEligibleGroupedBySubscription = $arrayPIMEligible.where({ $_.ScopeType -eq 'Sub' }) | Group-Object -Property ScopeId + + if ($summarySubscriptionsCount -gt 0) { + + $advisorScoreCategories = $arrayAdvisorScores.category | Sort-Object -Unique + $htAdvisorScoresSubscriptions = @{} + if ($advisorScoreCategories.Count -gt 0) { + $arrayAdvisorScoresGroupedBySubscriptionId = $arrayAdvisorScores | Group-Object -Property subscriptionId + foreach ($subEntry in $arrayAdvisorScoresGroupedBySubscriptionId) { + $htAdvisorScoresSubscriptions.($subEntry.Name) = @{} + foreach ($possibleCategory in $advisorScoreCategories) { + if ($subEntry.Group.category -eq $possibleCategory) { + $htAdvisorScoresSubscriptions.($subEntry.Name).($possibleCategory) = $subEntry.Group.where({ $_.category -eq $possibleCategory }).score + } + } + } + } + + $tfCount = $summarySubscriptionsCount + $htmlTableId = 'TenantSummary_subs' + $abbr = " " + [void]$htmlTenantSummary.AppendLine(@" + +
+ Supported Microsoft Azure offers docs
+ Understand Microsoft Defender for Cloud Secure Score Video , Blog , docs
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($possibleCategory in $advisorScoreCategories) { + if ($possibleCategory -eq 'Advisor') { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + } + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + [void]$htmlTenantSummary.AppendLine(@" + + +"@) + } + [void]$htmlTenantSummary.AppendLine(@' + + + + +'@) + + if (-not $ManagementGroupsOnly) { + if (-not $NoCsvExport) { + Write-Host " Exporting MDfC Email Notifications CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCEmailNotifications.csv'" + $htDefenderEmailContacts.values | Sort-Object -Property subscriptionName | Select-Object -Property subscriptionId, subscriptionName, alertNotificationsState, alertNotificationsminimalSeverity, roles, emails | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_MDfCEmailNotifications.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + + $subscriptionDetails4CSVExport = [System.Collections.ArrayList]@() + $htmlSUMMARYSubs = $null + $htmlSUMMARYSubs = foreach ($summarySubscription in $summarySubscriptions) { + $subPath = $htSubscriptionsMgPath.($summarySubscription.subscriptionId).ParentNameChainDelimited + $subscriptionTagsArray = [System.Collections.ArrayList]@() + foreach ($tag in ($htSubscriptionTags).($summarySubscription.subscriptionId).keys) { + $null = $subscriptionTagsArray.Add("'$($tag)':'$(($htSubscriptionTags).$($summarySubscription.subscriptionId).$tag)'") + } + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if ($htAzureConsumptionSubscriptions.($summarySubscription.subscriptionId)) { + if ([math]::Round($htAzureConsumptionSubscriptions.($summarySubscription.subscriptionId).TotalCost, 2) -eq 0) { + $totalCost = $htAzureConsumptionSubscriptions.($summarySubscription.subscriptionId).TotalCost.ToString('0.0000') + } + else { + $totalCost = (([math]::Round($htAzureConsumptionSubscriptions.($summarySubscription.subscriptionId).TotalCost, 2))).ToString('0.00') + } + $currency = $htAzureConsumptionSubscriptions.($summarySubscription.subscriptionId).Currency + } + else { + $totalCost = '0' + $currency = 'n/a' + } + } + else { + $totalCost = 'n/a' + $currency = 'n/a' + } + + if ($htDefenderEmailContacts.($summarySubscription.subscriptionId)) { + $hlpDefenderEmailContacts = $htDefenderEmailContacts.($summarySubscription.subscriptionId) + $MDfCEmailNotificationsState = $hlpDefenderEmailContacts.alertNotificationsState + $MDfCEmailNotificationsSeverity = $hlpDefenderEmailContacts.alertNotificationsminimalSeverity + $MDfCEmailNotificationsRoles = $hlpDefenderEmailContacts.roles + $MDfCEmailNotificationsEmails = $hlpDefenderEmailContacts.emails + } + else { + $MDfCEmailNotificationsState = '' + $MDfCEmailNotificationsSeverity = '' + $MDfCEmailNotificationsRoles = '' + $MDfCEmailNotificationsEmails = '' + } + + #rbac assignments owner and userAccountAdministrator + $rbacAtScopeForThisSubscription = ($rbacAllGroupedBySubscription.where( { $_.name -eq $summarySubscription.subscriptionId } )).group + + $rbacOwnersAtScopeForThisSubscription = ($rbacAtScopeForThisSubscription.where({ $_.Scope -eq 'thisScope Sub' -and $_.RoleId -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' })) + $rbacOwnersAtScopeForThisSubscriptionDirectCount = ($rbacOwnersAtScopeForThisSubscription.where( { $_.AssignmentType -eq 'direct' } )).Count + $rbacOwnersAtScopeForThisSubscriptionInDirectCount = $rbacOwnersAtScopeForThisSubscription.Count - $rbacOwnersAtScopeForThisSubscriptionDirectCount + + $rbacUAAsAtScopeForThisSubscription = ($rbacAtScopeForThisSubscription.where({ $_.Scope -eq 'thisScope Sub' -and $_.RoleId -eq '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' })) + $rbacUAAsAtScopeForThisSubscriptionDirectCount = ($rbacUAAsAtScopeForThisSubscription.where( { $_.AssignmentType -eq 'direct' } )).Count + $rbacUAAsAtScopeForThisSubscriptionInDirectCount = $rbacUAAsAtScopeForThisSubscription.Count - $rbacUAAsAtScopeForThisSubscriptionDirectCount + + #pim eligibility owner and userAccountAdministrator + $pimEligibleOwnersAtScopeForThisSubscriptionCount = '' + $pimEligibleUAAsAtScopeForThisSubscriptionCount = '' + if (-not $NoPIMEligibility) { + $pimEligibleAtScopeForThisSubscription = ($arrayPIMEligibleGroupedBySubscription.where( { $_.name -eq $summarySubscription.subscriptionId } )).group + $pimEligibleOwnersAtScopeForThisSubscriptionCount = ($pimEligibleAtScopeForThisSubscription.where( { $_.RoleIdGuid -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' } )).Count + $pimEligibleUAAsAtScopeForThisSubscriptionCount = ($pimEligibleAtScopeForThisSubscription.where( { $_.RoleIdGuid -eq '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' } )).Count + } + + $htColl = [ordered]@{} + @" + + + + + + + + + + + + + + + + + +"@ + + $htColl.Subscription = $summarySubscription.subscription + $htColl.SubscriptionId = $summarySubscription.subscriptionId + $htColl.QuotaId = $summarySubscription.SubscriptionQuotaId + $htColl.ManagementGroupPath = $subPath + $htColl.RoleAssignmentLimit = $htSubscriptionsRoleAssignmentLimit.($summarySubscription.subscriptionId) + $htColl.Tags = ($subscriptionTagsArray | Sort-Object) -join "$CsvDelimiterOpposite " + $htColl.'Owner(atScope)Direct' = $rbacOwnersAtScopeForThisSubscriptionDirectCount + $htColl.'Owner(atScope)Indirect' = $rbacOwnersAtScopeForThisSubscriptionInDirectCount + $htColl.'Owner(PIMEligibleAtScope)' = $pimEligibleOwnersAtScopeForThisSubscriptionCount + $htColl.'UserAccessAdministrator(atScope)Direct' = $rbacUAAsAtScopeForThisSubscriptionDirectCount + $htColl.'UserAccessAdministrator(atScope)Indirect' = $rbacUAAsAtScopeForThisSubscriptionInDirectCount + $htColl.'UserAccessAdministrator(PIMEligibleAtScope)' = $pimEligibleUAAsAtScopeForThisSubscriptionCount + $htColl.MDfCScore = $summarySubscription.SubscriptionASCSecureScore + $htColl.MDfCEmailNotificationsState = $MDfCEmailNotificationsState + $htColl.MDfCEmailNotificationsSeverity = $MDfCEmailNotificationsSeverity + $htColl.MDfCEmailNotificationsRoles = $MDfCEmailNotificationsRoles + $htColl.MDfCEmailNotificationsEmails = $MDfCEmailNotificationsEmails + + foreach ($possibleCategory in $advisorScoreCategories) { + if ($htAdvisorScoresSubscriptions.($summarySubscription.subscriptionId).($possibleCategory)) { + @" + +"@ + if ($possibleCategory -eq 'Advisor') { + $htColl.("$($possibleCategory)Score") = $htAdvisorScoresSubscriptions.($summarySubscription.subscriptionId).($possibleCategory) + } + else { + $htColl.("Advisor$($possibleCategory)Score") = $htAdvisorScoresSubscriptions.($summarySubscription.subscriptionId).($possibleCategory) + } + } + else { + @' + +'@ + $htColl.($possibleCategory) = 'n/a' + } + } + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + @" + + +"@ + $htColl."Cost($($AzureConsumptionPeriod)d)" = $totalCost + $htColl.Currency = $currency + } + @" + + +"@ + if (-not $NoCsvExport) { + $null = $subscriptionDetails4CSVExport.Add($htColl) + } + } + + if (-not $ManagementGroupsOnly) { + if (-not $NoCsvExport) { + Write-Host " Exporting SubscriptionDetails CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_SubscriptionDetails.csv'" + $subscriptionDetails4CSVExport | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_SubscriptionDetails.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubs) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdQuotaIdRole assignment limitTagsOwner (at Scope) directOwner (at Scope) indirect$($abbr)Owner (PIM eligible at scope)User Access Administrator (at Scope) directUser Access Administrator (at Scope) indirect$($abbr)User Access Administrator (PIM eligible at scope)MDfC ScoreMDfC 'Email notifications' stateMDfC 'Email notifications' severityMDfC 'Email notifications' rolesMDfC 'Email notifications' emails$possibleCategory scoreAdvisor $possibleCategory scoreCost ($($AzureConsumptionPeriod)d)CurrencyManagement Group Path
$($summarySubscription.subscription -replace '<', '<' -replace '>', '>')$($summarySubscription.subscriptionId)$($summarySubscription.SubscriptionQuotaId)$($htSubscriptionsRoleAssignmentLimit.($summarySubscription.subscriptionId))$(($subscriptionTagsArray | Sort-Object) -join "$CsvDelimiterOpposite ")$($rbacOwnersAtScopeForThisSubscriptionDirectCount)$($rbacOwnersAtScopeForThisSubscriptionInDirectCount)$($pimEligibleOwnersAtScopeForThisSubscriptionCount)$($rbacUAAsAtScopeForThisSubscriptionDirectCount)$($rbacUAAsAtScopeForThisSubscriptionInDirectCount)$($pimEligibleUAAsAtScopeForThisSubscriptionCount)$($summarySubscription.SubscriptionASCSecureScore)$($MDfCEmailNotificationsState)$($MDfCEmailNotificationsSeverity)$($MDfCEmailNotificationsRoles)$($MDfCEmailNotificationsEmails)$([math]::Round(($htAdvisorScoresSubscriptions.($summarySubscription.subscriptionId).($possibleCategory)), 2))n/a$totalCost$currency $subPath
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($summarySubscriptionsCount) Subscriptions

+"@) + } + + $endSUMMARYSubs = Get-Date + Write-Host " SUMMARYSubs duration: $((New-TimeSpan -Start $startSUMMARYSubs -End $endSUMMARYSubs).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSubs -End $endSUMMARYSubs).TotalSeconds) seconds)" + #endregion SUMMARYSubs + + #region SUMMARYOutOfScopeSubscriptions + Write-Host ' processing TenantSummary Subscriptions (out-of-scope)' + $outOfScopeSubscriptionsCount = ($outOfScopeSubscriptions).Count + if ($outOfScopeSubscriptionsCount -gt 0) { + $tfCount = $outOfScopeSubscriptionsCount + $htmlTableId = 'TenantSummary_outOfScopeSubscriptions' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYOutOfScopeSubscriptions = $null + $htmlSUMMARYOutOfScopeSubscriptions = foreach ($outOfScopeSubscription in $outOfScopeSubscriptions) { + @" + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYOutOfScopeSubscriptions) + [void]$htmlTenantSummary.AppendLine(@" + +
Subscription NameSubscriptionIdout-of-scope reasonManagement Group
$($outOfScopeSubscription.SubscriptionName)$($outOfScopeSubscription.SubscriptionId)$($outOfScopeSubscription.outOfScopeReason) $($outOfScopeSubscription.ManagementGroupName -replace '<', '<' -replace '>', '>') ($($outOfScopeSubscription.ManagementGroupId))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$outOfScopeSubscriptionsCount Subscriptions out-of-scope

+"@) + } + #endregion SUMMARYOutOfScopeSubscriptions + + #region SUMMARYTagNameUsage + Write-Host ' processing TenantSummary TagsUsage' + $tagsUsageCount = ($arrayTagList).Count + if ($tagsUsageCount -gt 0) { + $tagNamesUniqueCount = ($arrayTagList | Sort-Object -Property TagName -Unique).Count + $tagNamesUsedInScopes = ($arrayTagList.where( { $_.Scope -ne 'AllScopes' }) | Sort-Object -Property Scope -Unique).scope -join "$($CsvDelimiterOpposite) " + $tfCount = $tagsUsageCount + $htmlTableId = 'TenantSummary_tagsUsage' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Resource naming and tagging decision guide docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYtagsUsage = $null + $htmlSUMMARYtagsUsage = foreach ($tagEntry in $arrayTagList | Sort-Object -Property Scope, TagName -CaseSensitive) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYtagsUsage) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeTagNameCount
$($tagEntry.Scope)$($tagEntry.TagName)$($tagEntry.TagCount)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Tag Name Usage ($tagsUsageCount Tags) docs

+"@) + } + #endregion SUMMARYTagNameUsage + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region SUMMARYResources + $startSUMMARYResources = Get-Date + Write-Host ' processing TenantSummary Subscriptions Resources' + if (($resourcesAll).count -gt 0) { + $resourcesAllGroupedByType = $resourcesAll | Select-Object -Property type, count_ | Group-Object type + $resourcesTotal = ($resourcesAll.count_ | Measure-Object -Sum).Sum + $resourcesResourceTypeCount = ($resourcesAll.type | Sort-Object -Unique).Count + + if ($resourcesResourceTypeCount -gt 0) { + $tfCount = ($resourcesAllGroupedByType | Measure-Object).Count + $htmlTableId = 'TenantSummary_resources' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + +"@) + $htmlSUMMARYResources = $null + $htmlSUMMARYResources = foreach ($resourceAllSummarized in $resourcesAllGroupedByType) { + $type = $resourceAllSummarized.Name + $script:htDailySummary."ResourceType_$($resourceAllSummarized.Name)" = ($resourceAllSummarized.group.count_ | Measure-Object -Sum).Sum + @" + + + + +"@ + + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYResources) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeResource Count
$($type)$(($resourceAllSummarized.group.count_ | Measure-Object -Sum).Sum)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Resources ($resourcesResourceTypeCount ResourceTypes)

+"@) + } + + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

Resources (0 ResourceTypes)

+'@) + } + $endSUMMARYResources = Get-Date + Write-Host " SUMMARY Resources processing duration: $((New-TimeSpan -Start $startSUMMARYResources -End $endSUMMARYResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYResources -End $endSUMMARYResources).TotalSeconds) seconds)" + #endregion SUMMARYResources + + #region SUMMARYResourcesByLocation + $startSUMMARYResources = Get-Date + Write-Host ' processing TenantSummary Subscriptions Resources by Location' + if (($resourcesAll | Measure-Object).count -gt 0) { + $resourcesAllGroupedByTypeLocation = $resourcesAll | Select-Object -Property type, location, count_ | Group-Object type, location + $resourcesTotal = ($resourcesAll.count_ | Measure-Object -Sum).Sum + $resourcesResourceTypeCount = ($resourcesAll.type | Sort-Object -Unique).Count + $resourcesLocationCount = ($resourcesAll.location | Sort-Object -Unique).Count + + if ($resourcesResourceTypeCount -gt 0) { + $tfCount = ($resourcesAllGroupedByTypeLocation | Measure-Object).Count + $htmlTableId = 'TenantSummary_resourcesByLocation' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYResources = $null + $htmlSUMMARYResources = foreach ($resourceAllSummarized in $resourcesAllGroupedByTypeLocation) { + $typeLocation = $resourceAllSummarized.Name.Split(', ') + $type = $typeLocation[0] + $location = $typeLocation[1] + @" + + + + + +"@ + + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYResources) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeLocationResource Count
$($type)$($location)$(($resourceAllSummarized.group.count_ | Measure-Object -Sum).Sum)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Resources ($resourcesResourceTypeCount ResourceTypes)

+"@) + } + + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

Resources (0 ResourceTypes)

+'@) + } + $endSUMMARYResources = Get-Date + Write-Host " SUMMARY Resources ByLocation processing duration: $((New-TimeSpan -Start $startSUMMARYResources -End $endSUMMARYResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYResources -End $endSUMMARYResources).TotalSeconds) seconds)" + #endregion SUMMARYResourcesByLocation + + #region SUMMARYResourceFluctuation + $startSUMMARYResourceFluctuation = Get-Date + Write-Host ' processing TenantSummary Resource fluctuation' + if (($arrayResourceFluctuationFinal).count -gt 0) { + $resourceTypesCount = ($arrayResourceFluctuationFinal | Group-Object -Property ResourceType | Measure-Object).Count + $addedCount = ($arrayResourceFluctuationFinal.where({ $_.Event -eq 'Added' }).'Resource count' | Measure-Object -Sum).Sum + $removedCount = ($arrayResourceFluctuationFinal.where({ $_.Event -eq 'Removed' }).'Resource count' | Measure-Object -Sum).Sum + + $tfCount = ($arrayResourceFluctuationFinal).count + $htmlTableId = 'TenantSummary_resourceFluctuation' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + $htmlSUMMARYResourceFluctuation = $null + $htmlSUMMARYResourceFluctuation = foreach ($entry in $arrayResourceFluctuationFinal | Sort-Object -Property ResourceType, Event) { + @" + + + + + + +"@ + + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYResourceFluctuation) + [void]$htmlTenantSummary.AppendLine(@" + +
EventResourceTypeResource countSubscription count
$($entry.Event)$($entry.ResourceType)$($entry.'Resource count')$($entry.'Subscription count')
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Resource fluctuation since last run

+'@) + } + $endSUMMARYResourceFluctuation = Get-Date + Write-Host " SUMMARY Resource fluctuation processing duration: $((New-TimeSpan -Start $startSUMMARYResourceFluctuation -End $endSUMMARYResourceFluctuation).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYResourceFluctuation -End $endSUMMARYResourceFluctuation).TotalSeconds) seconds)" + #endregion SUMMARYResourceFluctuation + + #region SUMMARYCAFResourceNamingALL + $startSUMMARYCAFResourceNamingALL = Get-Date + Write-Host ' processing TenantSummary CAFResourceNamingALL' + $script:resourcesIdsAllCAFNamingRelevant = $resourcesIdsAll.where({ $_.cafResourceNamingResult -ne 'n/a' }) + $resourcesIdsAllCAFNamingRelevantGroupedByType = $resourcesIdsAllCAFNamingRelevant | Group-Object -Property type + $resourcesIdsAllCAFNamingRelevantGroupedByTypeCount = ($resourcesIdsAllCAFNamingRelevantGroupedByType | Measure-Object).Count + + if ($resourcesIdsAllCAFNamingRelevantGroupedByTypeCount -gt 0) { + + $tfCount = $resourcesIdsAllCAFNamingRelevantGroupedByTypeCount + $htmlTableId = 'TenantSummary_CAFResourceNamingALL' + [void]$htmlTenantSummary.AppendLine(@" + +
+ CAF - Recommended abbreviations for Azure resource types docs
+ Resource details can be found in the CSV output *_ResourcesAll.csv
+ Download CSV semicolon | comma + + + + + + + + + + + + +"@) + + $htmlSUMMARYCAFResourceNamingALL = $null + $htmlSUMMARYCAFResourceNamingALL = foreach ($entry in $resourcesIdsAllCAFNamingRelevantGroupedByType) { + + $resourceTypeGroupedByCAFResourceNamingResult = $entry.Group | Group-Object -Property cafResourceNamingResult, cafResourceNaming + if ($entry.Group.cafResourceNaming.Count -gt 1) { + $namingConvention = ($entry.Group.cafResourceNaming)[0] + $namingConventionFriendlyName = ($entry.Group.cafResourceNamingFriendlyName)[0] + } + else { + $namingConvention = $entry.Group.cafResourceNaming + $namingConventionFriendlyName = $entry.Group.cafResourceNamingFriendlyName + } + + $passed = 0 + $failed = 0 + foreach ($result in $resourceTypeGroupedByCAFResourceNamingResult) { + $resultNameSplitted = $result.Name -split ', ' + if ($resultNameSplitted[0] -eq 'passed') { + $passed = $result.Count + } + + if ($resultNameSplitted[0] -eq 'failed') { + $failed = $result.Count + } + } + + if ($passed -gt 0) { + $percentage = [math]::Round(($passed / ($passed + $failed) * 100), 2) + } + else { + $percentage = 0 + } + + @" + + + + + + + + +"@ + + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYCAFResourceNamingALL) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeRecommendationResourceFriendlyNamepassedfailedpassed percentage
$($entry.Name)$($namingConvention)$($namingConventionFriendlyName)$($passed)$($failed)$($percentage)%
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No CAF Naming Recommendation Compliance data

+'@) + } + $endSUMMARYCAFResourceNamingALL = Get-Date + Write-Host " SUMMARY CAFResourceNamingALL processing duration: $((New-TimeSpan -Start $startSUMMARYCAFResourceNamingALL -End $endSUMMARYCAFResourceNamingALL).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYCAFResourceNamingALL -End $endSUMMARYCAFResourceNamingALL).TotalSeconds) seconds)" + #endregion SUMMARYCAFResourceNamingALL + } + + #region SUMMARYOrphanedResources + $startSUMMARYOrphanedResources = Get-Date + Write-Host ' processing TenantSummary Orphaned/unused Resources' + if ($arrayOrphanedResources.count -gt 0) { + $script:arrayOrphanedResourcesSlim = $arrayOrphanedResources | Sort-Object -Property type + + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + $orphanedIncludingCost = $true + $hintTableTH = " ($($AzureConsumptionPeriod) days)" + + $arrayOrphanedResourcesGroupedByType = $arrayOrphanedResourcesSlim | Group-Object type, currency + $orphanedResourceTypesCount = ($arrayOrphanedResourcesGroupedByType | Measure-Object).Count + $orphanedResourceTypesCountUnique = ($arrayOrphanedResourcesSlim.type | Sort-Object -Unique).Count + } + else { + $orphanedIncludingCost = $false + $hintTableTH = '' + + $arrayOrphanedResourcesGroupedByType = $arrayOrphanedResourcesSlim | Group-Object type + $orphanedResourceTypesCount = ($arrayOrphanedResourcesGroupedByType | Measure-Object).Count + $orphanedResourceTypesCountUnique = ($arrayOrphanedResourcesSlim.type | Sort-Object -Unique).Count + } + + $tfCount = $orphanedResourceTypesCount + $htmlTableId = 'TenantSummary_orphanedResources' + [void]$htmlTenantSummary.AppendLine(@" + +
+ 'Azure Orphan Resources' ARG queries and workbooks GitHub
+ Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
+ Download CSV semicolon | comma + + + + + + + + + + + + +"@) + + $htmlSUMMARYOrphanedResources = $null + $htmlSUMMARYOrphanedResources = foreach ($orphanedResourceType in $arrayOrphanedResourcesGroupedByType | Sort-Object -Property Name) { + $script:htDailySummary."OrpanedResourceType_$($orphanedResourceType.Name)" = ($orphanedResourceType.count) + if ($orphanedIncludingCost) { + if (($orphanedResourceType.Group[0].Intent) -like 'cost savings*') { + $orphCost = ($orphanedResourceType.Group.Cost | Measure-Object -Sum).Sum + if ($orphCost -eq 0) { + $orphCost = '' + } + $orphCurrency = $orphanedResourceType.Group[0].Currency + $script:htDailySummary."OrpanedResourceType_$($orphanedResourceType.Name)_Costs" = $orphCost + $script:htDailySummary."OrpanedResourceType_$($orphanedResourceType.Name)_Costs_ConsumptionPeriodInDays" = $AzureConsumptionPeriod + } + else { + $orphCost = '' + $orphCurrency = '' + } + + } + else { + if (($orphanedResourceType.Group.Intent | Get-Unique) -like 'cost savings*') { + $orphCost = "use parameter -DoAzureConsumption to show potential savings" + $orphCurrency = '' + } + else { + $orphCost = '' + $orphCurrency = '' + } + } + + @" + + + + + + + + +"@ + + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYOrphanedResources) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeResource countSubscriptions countIntentCost$($hintTableTH)Currency
$(($orphanedResourceType.Name -split ',')[0])$($orphanedResourceType.count)$(($orphanedResourceType.Group.SubscriptionId | Sort-Object -Unique).Count)$($orphanedResourceType.Group[0].Intent)$($orphCost)$($orphCurrency)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No cost optimization & cleanup

+'@) + } + $endSUMMARYOrphanedResources = Get-Date + Write-Host " SUMMARY Orphaned/unused Resources processing duration: $((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalSeconds) seconds)" + #endregion SUMMARYOrphanedResources + + #region SUMMARYSubResourceProviders + if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { + $startSUMMARYSubResourceProviders = Get-Date + Write-Host ' processing TenantSummary Subscriptions Resource Providers' + $resourceProvidersAllCount = (($htResourceProvidersAll).Keys | Measure-Object).count + if ($resourceProvidersAllCount -gt 0) { + $grped = (($htResourceProvidersAll).values.Providers) | Sort-Object -Property namespace, registrationState | Group-Object namespace + $htResProvSummary = @{} + foreach ($grp in $grped) { + $htResProvSummary.($grp.name) = @{} + $regstates = ($grp.group | Sort-Object -Property registrationState -Unique).registrationstate + foreach ($regstate in $regstates) { + $htResProvSummary.($grp.name).$regstate = (($grp.group).where( { $_.registrationstate -eq $regstate }) | Measure-Object).count + } + } + $providerSummary = [System.Collections.ArrayList]@() + foreach ($provider in $htResProvSummary.keys) { + $hlperProvider = $htResProvSummary.$provider + if ($hlperProvider.registered) { + $registered = $hlperProvider.registered + } + else { + $registered = '0' + } + + if ($hlperProvider.registering) { + $registering = $hlperProvider.registering + } + else { + $registering = '0' + } + + if ($hlperProvider.notregistered) { + $notregistered = $hlperProvider.notregistered + } + else { + $notregistered = '0' + } + + if ($hlperProvider.unregistering) { + $unregistering = $hlperProvider.unregistering + } + else { + $unregistering = '0' + } + + $null = $providerSummary.Add([PSCustomObject]@{ + Provider = $provider + Registered = $registered + NotRegistered = $notregistered + Registering = $registering + Unregistering = $unregistering + }) + } + + $uniqueNamespaces = (($htResourceProvidersAll).values.Providers) | Sort-Object -Property namespace -Unique + $uniqueNamespacesCount = ($uniqueNamespaces | Measure-Object).count + $uniqueNamespaceRegistrationState = (($htResourceProvidersAll).values.Providers) | Sort-Object -Property namespace, registrationState -Unique + $providersRegistered = ($uniqueNamespaceRegistrationState.where( { $_.registrationState -eq 'registered' -or $_.registrationState -eq 'registering' }) | Sort-Object namespace -Unique).namespace + $providersRegisteredCount = ($providersRegistered | Measure-Object).count + + $providersNotRegisteredUniqueCount = 0 + foreach ($uniqueNamespace in $uniqueNamespaces) { + if ($providersRegistered -notcontains ($uniqueNamespace.namespace)) { + $providersNotRegisteredUniqueCount++ + } + } + $tfCount = $uniqueNamespacesCount + $htmlTableId = 'TenantSummary_SubResourceProviders' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYSubResourceProviders = $null + $htmlSUMMARYSubResourceProviders = foreach ($provider in ($providerSummary | Sort-Object -Property Provider)) { + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubResourceProviders) + [void]$htmlTenantSummary.AppendLine(@" + +
ProviderRegisteredRegisteringNotRegisteredUnregistering
$($provider.Provider)$($provider.Registered)$($provider.Registering)$($provider.NotRegistered)$($provider.Unregistering)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$resourceProvidersAllCount Resource Providers

+"@) + } + $endSUMMARYSubResourceProviders = Get-Date + Write-Host " TenantSummary Subscriptions Resource Providers duration: $((New-TimeSpan -Start $startSUMMARYSubResourceProviders -End $endSUMMARYSubResourceProviders).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYSubResourceProviders -End $endSUMMARYSubResourceProviders).TotalSeconds) seconds)" + } + #endregion SUMMARYSubResourceProviders + + #region SUMMARYSubResourceProvidersDetailed + if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $false) { + if ($azAPICallConf['htParameters'].NoResourceProvidersDetailed -eq $false) { + + Write-Host ' processing TenantSummary Subscriptions Resource Providers detailed' + $startsumRPDetailed = Get-Date + $resourceProvidersAllCount = (($htResourceProvidersAll).Keys).count + if ($resourceProvidersAllCount -gt 0) { + $tfCount = ($htResourceProvidersAll).values.Providers.Count + if ($tfCount -lt $HtmlTableRowsLimit) { + $htmlTableId = 'TenantSummary_SubResourceProvidersDetailed' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + + } + else { + Write-Host " !Skipping TenantSummary ResourceProvidersDetailed HTML processing as $tfCount lines is exceeding the critical rows limit of $HtmlTableRowsLimit" -ForegroundColor Yellow + } + $cnter = 0 + $startResProvDetailed = Get-Date + $htmlSUMMARYSubResourceProvidersDetailed = $null + + $arrayResourceProvidersDetailedForCSVExport = [System.Collections.ArrayList]@() + $htmlSUMMARYSubResourceProvidersDetailed = foreach ($subscriptionResProv in (($htResourceProvidersAll).Keys | Sort-Object)) { + $subscriptionResProvDetails = $htSubscriptionsMgPath.($subscriptionResProv) + foreach ($provider in ($htResourceProvidersAll).($subscriptionResProv).Providers | Sort-Object @{Expression = { $_.namespace } }) { + $cnter++ + if ($cnter % 1000 -eq 0) { + $etappeResProvDetailed = Get-Date + Write-Host " $cnter ResProv processed; $((New-TimeSpan -Start $startResProvDetailed -End $etappeResProvDetailed).TotalSeconds) seconds" + } + + #array for exportCSV + if (-not $NoCsvExport) { + $null = $arrayResourceProvidersDetailedForCSVExport.Add([PSCustomObject]@{ + Subscription = $subscriptionResProvDetails.DisplayName + SubscriptionId = $subscriptionResProv + SubscriptionMGpath = $subscriptionResProvDetails.pathDelimited + Provider = $provider.namespace + State = $provider.registrationState + }) + } + + @" + + + + + + + +"@ + } + } + + #region exportCSV + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_ResourceProviders" + Write-Host " Exporting ResourceProviders CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + $arrayResourceProvidersDetailedForCSVExport | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + $arrayResourceProvidersDetailedForCSVExport = $null + } + #endregion exportCSV + + if ($tfCount -lt $HtmlTableRowsLimit) { + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubResourceProvidersDetailed) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdSubscription MG path +ProviderState
$($subscriptionResProvDetails.DisplayName)$($subscriptionResProv)$($subscriptionResProvDetails.pathDelimited)$($provider.namespace)$($provider.registrationState)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" + +
+ Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
+ You can adjust the html row limit by using parameter -HtmlTableRowsLimit
+ Check the parameters documentation Azure Governance Visualizer docs +
+"@) + } + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$resourceProvidersAllCount Resource Providers

+"@) + } + $endsumRPDetailed = Get-Date + Write-Host " RP detailed processing duration: $((New-TimeSpan -Start $startsumRPDetailed -End $endsumRPDetailed).TotalMinutes) minutes ($((New-TimeSpan -Start $startsumRPDetailed -End $endsumRPDetailed).TotalSeconds) seconds)" + } + } + #endregion SUMMARYSubResourceProvidersDetailed + + #region SUMMARYSubFeatures + Write-Host ' processing TenantSummary Subscriptions Features' + $startSubFeatures = Get-Date + $subFeaturesAllCount = $arrayFeaturesAll.count + if ($subFeaturesAllCount -gt 0) { + + #region exportCSV + if (-not $NoCsvExport) { + $csvFilename = "$($filename)_SubscriptionsFeatures" + Write-Host " Exporting SubscriptionsFeatures CSV '$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv'" + ($arrayFeaturesAll | Select-Object -ExcludeProperty mgPathArray | Sort-Object -Property feature, subscriptionId) | Export-Csv -Encoding utf8 -Path "$($outputPath)$($DirectorySeparatorChar)$($csvFilename).csv" -Delimiter $csvDelimiter -NoTypeInformation + } + #endregion exportCSV + + $subFeaturesGroupedByFeature = $arrayFeaturesAll | Group-Object -Property feature + $tfCount = ($subFeaturesGroupedByFeature | Measure-Object).Count + $htmlTableId = 'TenantSummary_SubFeatures' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Set up preview features in Azure subscription docs
+ Download CSV semicolon | comma + + + + + + + + +"@) + + + $cnter = 0 + $startResProvDetailed = Get-Date + $htmlSUMMARYSubFeatures = $null + $htmlSUMMARYSubFeatures = foreach ($feature in $subFeaturesGroupedByFeature | Sort-Object -Property name) { + @" + + + + +"@ + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubFeatures) + [void]$htmlTenantSummary.AppendLine(@" + +
FeatureSubscriptions
$($feature.name)$($feature.Count)
+
+ +"@) + + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No enabled Subscriptions Features docs

+'@) + } + $endSubFeatures = Get-Date + Write-Host " Subscriptions Features processing duration: $((New-TimeSpan -Start $startSubFeatures -End $endSubFeatures).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubFeatures -End $endSubFeatures).TotalSeconds) seconds)" + #endregion SUMMARYSubFeatures + + #region SUMMARYSubResourceLocks + Write-Host ' processing TenantSummary Subscriptions Resource Locks' + $tfCount = 6 + $startResourceLocks = Get-Date + + if (($htResourceLocks.keys | Measure-Object).Count -gt 0) { + $htmlTableId = 'TenantSummary_ResourceLocks' + + $subscriptionLocksCannotDeleteCount = ($htResourceLocks.Keys.where( { $htResourceLocks.($_).SubscriptionLocksCannotDeleteCount -gt 0 } )).Count + $subscriptionLocksReadOnlyCount = ($htResourceLocks.Keys.where( { $htResourceLocks.($_).SubscriptionLocksReadOnlyCount -gt 0 } )).Count + + $resourceGroupsLocksCannotDeleteCount = ($htResourceLocks.Keys.where( { $htResourceLocks.($_).ResourceGroupsLocksCannotDeleteCount -gt 0 } )).Count + $resourceGroupsLocksReadOnlyCount = ($htResourceLocks.Keys.where({ $htResourceLocks.($_).ResourceGroupsLocksReadOnlyCount -gt 0 } )).Count + + $resourcesLocksCannotDeleteCount = ($htResourceLocks.Keys.where( { $htResourceLocks.($_).ResourcesLocksCannotDeleteCount -gt 0 } )).Count + $resourcesLocksReadOnlyCount = ($htResourceLocks.Keys.where( { $htResourceLocks.($_).ResourcesLocksReadOnlyCount -gt 0 } )).Count + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Considerations before applying locks docs
+ Note: Detailed information on Resource Locks is provided in the *_ResourceLocks.csv + + + + + + + + + + + + + + + + +
Lock scopeLock typepresence
SubscriptionCannotDelete$($subscriptionLocksCannotDeleteCount) of $totalSubCount Subscriptions
SubscriptionReadOnly$($subscriptionLocksReadOnlyCount) of $totalSubCount Subscriptions
ResourceGroupCannotDelete$($resourceGroupsLocksCannotDeleteCount) of $totalSubCount Subscriptions (total: $(($htResourceLocks.Values.ResourceGroupsLocksCannotDeleteCount | Measure-Object -Sum).Sum))
ResourceGroupReadOnly$($resourceGroupsLocksReadOnlyCount) of $totalSubCount Subscriptions (total: $(($htResourceLocks.Values.ResourceGroupsLocksReadOnlyCount | Measure-Object -Sum).Sum))
ResourceCannotDelete$($resourcesLocksCannotDeleteCount) of $totalSubCount Subscriptions (total: $(($htResourceLocks.Values.ResourcesLocksCannotDeleteCount | Measure-Object -Sum).Sum))
ResourceReadOnly$($resourcesLocksReadOnlyCount) of $totalSubCount Subscriptions (total: $(($htResourceLocks.Values.ResourcesLocksReadOnlyCount | Measure-Object -Sum).Sum))
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Resource Locks at all docs

+'@) + } + $endResourceLocks = Get-Date + Write-Host " ResourceLocks processing duration: $((New-TimeSpan -Start $startResourceLocks -End $endResourceLocks).TotalMinutes) minutes ($((New-TimeSpan -Start $startResourceLocks -End $endResourceLocks).TotalSeconds) seconds)" + #endregion SUMMARYSubResourceLocks + + #SUMMARYSubDefenderPlansSubscriptionsSkipped + if ($arrayDefenderPlansSubscriptionsSkipped.Count -gt 0) { + #region SUMMARYSubDefenderPlansSubscriptionsSkipped + Write-Host ' processing TenantSummary Subscriptions Microsoft Defender for Cloud plans SubscriptionsSkipped' + + $tfCount = $defenderPlansGroupedByPlanCount + $startDefenderPlans = Get-Date + + $htmlTableId = 'TenantSummary_DefenderPlansSubscriptionsSkipped' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Register Resource Provider 'Microsoft.Security' docs
+ Microsoft Defender for Cloud's enhanced security features docs
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + + foreach ($subscription in $arrayDefenderPlansSubscriptionsSkipped | Sort-Object -Property subscriptionName) { + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + +"@) + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Subscription NameSubscription IdSubscription QuotaIdSubscription MG pathreason
$($subscription.subscriptionName)$($subscription.subscriptionId)$($subscription.subscriptionQuotaId)$($subscription.subscriptionMgPath)$($subscription.reason)
+ +
+"@) + + $endDefenderPlans = Get-Date + Write-Host " Microsoft Defender for Cloud plans SubscriptionsSkipped processing duration: $((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalSeconds) seconds)" + #endregion SUMMARYSubDefenderPlansSubscriptionsSkipped + } + + #region SUMMARYSubDefenderPlansByPlan + Write-Host ' processing TenantSummary Subscriptions Microsoft Defender for Cloud plans by plan' + + $tfCount = $defenderPlansGroupedByPlanCount + $startDefenderPlans = Get-Date + + if ($defenderPlansGroupedByPlanCount -gt 0) { + $htmlTableId = 'TenantSummary_DefenderPlans' + + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + if ($defenderPlanDeprecatedContainerRegistry) { + [void]$htmlTenantSummary.AppendLine(@' + Using deprecated plan 'Container registries' docs
+'@) + } + if ($defenderPlanDeprecatedKubernetesService) { + [void]$htmlTenantSummary.AppendLine(@' + Using deprecated plan 'Kubernetes' docs
+'@) + } + + [void]$htmlTenantSummary.AppendLine(@" + Microsoft Defender for Cloud's enhanced security features docs
+ Download CSV semicolon | comma + + + + + + + + +"@) + + foreach ($defenderCapabilityAndTier in $defenderPlansGroupedByPlan | Sort-Object -Property Name) { + if ($defenderCapabilityAndTier.Name -eq 'ContainerRegistry, Standard' -or $defenderCapabilityAndTier.Name -eq 'KubernetesService, Standard') { + $thisDefenderPlan = " $($defenderCapabilityAndTier.Name)" + } + else { + $thisDefenderPlan = $defenderCapabilityAndTier.Name + } + [void]$htmlTenantSummary.AppendLine(@" + + + + +"@) + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Plan/TierSubscription Count
$($thisDefenderPlan)$($defenderCapabilityAndTier.Count)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Microsoft Defender for Cloud plans at all

+'@) + } + $endDefenderPlans = Get-Date + Write-Host " Microsoft Defender for Cloud plans by plan processing duration: $((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalSeconds) seconds)" + #endregion SUMMARYSubDefenderPlansByPlan + + #region SUMMARYSubDefenderPlansBySubscription + Write-Host ' processing TenantSummary Subscriptions Microsoft Defender for Cloud plans by Subscription' + $tfCount = $subsDefenderPlansCount + $startDefenderPlans = Get-Date + + if (($arrayDefenderPlans).Count -gt 0) { + $htmlTableId = 'TenantSummary_DefenderPlansBySubscription' + + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + if ($defenderPlanDeprecatedContainerRegistry) { + [void]$htmlTenantSummary.AppendLine(@' + Using deprecated plan 'Container registries' docs
+'@) + } + if ($defenderPlanDeprecatedKubernetesService) { + [void]$htmlTenantSummary.AppendLine(@' + Using deprecated plan 'Kubernetes' docs
+'@) + } + + [void]$htmlTenantSummary.AppendLine(@" + Microsoft Defender for Cloud's enhanced security features docs
+ Download CSV semicolon | comma + + + + + + +"@) + + foreach ($defenderCapability in $defenderCapabilities) { + if (($defenderPlanDeprecatedContainerRegistry -and $defenderCapability -eq 'ContainerRegistry') -or ($defenderPlanDeprecatedKubernetesService -and $defenderCapability -eq 'KubernetesService')) { + $thisDefenderCapability = " $($defenderCapability)" + } + else { + $thisDefenderCapability = $defenderCapability + } + [void]$htmlTenantSummary.AppendLine(@" + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@' + + + +'@) + + foreach ($sub in $defenderPlansGroupedBySub) { + $nameSplit = $sub.Name.split(', ') + [void]$htmlTenantSummary.AppendLine(@" + + + + + +"@) + + foreach ($plan in $sub.Group | Sort-Object -Property defenderPlan) { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + [void]$htmlTenantSummary.AppendLine(@' + +'@) + } + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdSubscription MG path$($thisDefenderCapability)
$($nameSplit[0])$($nameSplit[1])$($nameSplit[2])$($plan.defenderPlanTier)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Microsoft Defender for Cloud plans at all

+'@) + } + $endDefenderPlans = Get-Date + Write-Host " Microsoft Defender for Cloud plans by Subscription processing duration: $((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalMinutes) minutes ($((New-TimeSpan -Start $startDefenderPlans -End $endDefenderPlans).TotalSeconds) seconds)" + #endregion SUMMARYSubDefenderPlansBySubscription + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region SUMMARYSubUserAssignedIdentities4Resources + Write-Host ' processing TenantSummary Subscriptions UserAssigned Managed Identities assigned to Resources' + $arrayUserAssignedIdentities4ResourcesCount = $arrayUserAssignedIdentities4Resources.Count + $tfCount = $arrayUserAssignedIdentities4ResourcesCount + $startUserAssignedIdentities4Resources = Get-Date + + if ($arrayUserAssignedIdentities4ResourcesCount -gt 0) { + + $script:htUserAssignedIdentitiesAssignedResources = @{} + $script:htResourcesAssignedUserAssignedIdentities = @{} + foreach ($entry in $arrayUserAssignedIdentities4Resources) { + #UserAssignedIdentities + if (-not $htUserAssignedIdentitiesAssignedResources.($entry.miPrincipalId)) { + $script:htUserAssignedIdentitiesAssignedResources.($entry.miPrincipalId) = @{} + $script:htUserAssignedIdentitiesAssignedResources.($entry.miPrincipalId).ResourcesCount = 1 + } + else { + $script:htUserAssignedIdentitiesAssignedResources.($entry.miPrincipalId).ResourcesCount++ + } + #Resources + if (-not $htResourcesAssignedUserAssignedIdentities.(($entry.resourceId).tolower())) { + $script:htResourcesAssignedUserAssignedIdentities.(($entry.resourceId).tolower()) = @{} + $script:htResourcesAssignedUserAssignedIdentities.(($entry.resourceId).tolower()).UserAssignedIdentitiesCount = 1 + } + else { + $script:htResourcesAssignedUserAssignedIdentities.(($entry.resourceId).tolower()).UserAssignedIdentitiesCount++ + } + } + + $htmlTableId = 'TenantSummary_UserAssignedIdentities4Resources' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Managed identity 'user-assigned' vs 'system-assigned' docs
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + +"@) + + [void]$htmlTenantSummary.AppendLine(@' + + + +'@) + + $userAssignedIdentities4Resources4CSVExport = [System.Collections.ArrayList]@() + foreach ($miResEntry in $arrayUserAssignedIdentities4Resources | Sort-Object -Property miResourceId, resourceId) { + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + +"@) + + if (-not $NoCsvExport) { + $null = $userAssignedIdentities4Resources4CSVExport.Add([PSCustomObject]@{ + MIName = $miResEntry.miResourceName + MIMgPath = $miResEntry.miMgPath + MISubscriptionName = $miResEntry.miSubscriptionName + MISubscriptionId = $miResEntry.miSubscriptionId + MIResourceGroup = $miResEntry.miResourceGroupName + MIResourceId = $miResEntry.miResourceId + MIAADSPObjectId = $miResEntry.miPrincipalId + MIAADSPApplicationId = $miResEntry.miClientId + MICountResAssignments = $htUserAssignedIdentitiesAssignedResources.($miResEntry.miPrincipalId).ResourcesCount + MICrossSubscription = $miResEntry.miCrossSubscription + ResName = $miResEntry.resourceName + ResType = $miResEntry.resourceType + ResMgPath = $miResEntry.resourceMgPath + ResSubscriptionName = $miResEntry.resourceSubscriptionName + ResSubscriptionId = $miResEntry.resourceSubscriptionId + ResResourceGroup = $miResEntry.resourceResourceGroupName + ResId = $miResEntry.resourceId + ResCountAssignedMIs = $htResourcesAssignedUserAssignedIdentities.(($miResEntry.resourceId).tolower()).UserAssignedIdentitiesCount + }) + } + + } + + if (-not $NoCsvExport) { + Write-Host " Exporting UserAssignedIdentities4Resources CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_UserAssignedIdentities4Resources.csv'" + $userAssignedIdentities4Resources4CSVExport | Sort-Object -Property MIResourceId, ResId | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_UserAssignedIdentities4Resources.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + [void]$htmlTenantSummary.AppendLine(@" + +
MI NameMI MgPathMI Subscription NameMI Subscription IdMI ResourceGroupMI ResourceIdMI AAD SP objectIdMI AAD SP applicationIdMI count Res assignmentsMI used cross subscriptionRes NameRes TypeRes MgPathRes Subscription NameRes Subscription IdRes ResourceGroupRes IdRes count assigned MIs
$($miResEntry.miResourceName)$($miResEntry.miMgPath)$($miResEntry.miSubscriptionName)$($miResEntry.miSubscriptionId)$($miResEntry.miResourceGroupName)$($miResEntry.miResourceId)$($miResEntry.miPrincipalId)$($miResEntry.miClientId)$($htUserAssignedIdentitiesAssignedResources.($miResEntry.miPrincipalId).ResourcesCount)$($miResEntry.miCrossSubscription)$($miResEntry.resourceName)$($miResEntry.resourceType)$($miResEntry.resourceMgPath)$($miResEntry.resourceSubscriptionName)$($miResEntry.resourceSubscriptionId)$($miResEntry.resourceResourceGroupName)$($miResEntry.resourceId)$($htResourcesAssignedUserAssignedIdentities.(($miResEntry.resourceId).tolower()).UserAssignedIdentitiesCount)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No UserAssigned Managed Identities assigned to Resources / vice versa - at all

+'@) + } + $endUserAssignedIdentities4Resources = Get-Date + Write-Host " UserAssigned Managed Identities assigned to Resources processing duration: $((New-TimeSpan -Start $startUserAssignedIdentities4Resources -End $endUserAssignedIdentities4Resources).TotalMinutes) minutes ($((New-TimeSpan -Start $startUserAssignedIdentities4Resources -End $endUserAssignedIdentities4Resources).TotalSeconds) seconds)" + #endregion SUMMARYSubUserAssignedIdentities4Resources + + #region SUMMARYPSRule + if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { + $startPSRule = Get-Date + Write-Host ' processing TenantSummary PSRule' + $arrayPSRuleCount = $arrayPsRule.Count + + if ($arrayPSRuleCount -gt 0) { + + if (-not $NoCsvExport) { + $PSRuleCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PSRule.csv" + Write-Host " Exporting 'PSRule for Azure' CSV '$PSRuleCSVPath'" + $arrayPsRule | Sort-Object -Property resourceId, pillar, category, severity, rule, recommendation | Export-Csv -Path $PSRuleCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + + if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { + $exportCSVPSRuleFileSize = (Get-Item -Path $PSRuleCSVPath).length / 1MB + if ($exportCSVPSRuleFileSize -gt 100) { + Write-Host " The exported 'PSRule for Azure' CSV '$PSRuleCSVPath' exceeds the GitHub file limit of 100MB" + Write-Host ' more info: https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github#file-size-limits' + Write-Host ' ! ---> Hint: Consider using additional parameter -PSRuleFailedOnly / results will only include failed resources' + Write-Host " Re-Exporting 'PSRule for Azure' CSV '$PSRuleCSVPath' excluding column 'description'" + $arrayPsRule | Select-Object -ExcludeProperty description | Sort-Object -Property resourceId, pillar, category, severity, rule, recommendation | Export-Csv -Path "$PSRuleCSVPath" -Delimiter "$csvDelimiter" -NoTypeInformation + + $exportCSVPSRuleFileSize = (Get-Item -Path $PSRuleCSVPath).length / 1MB + if ($exportCSVPSRuleFileSize -gt 100) { + Write-Host " The exported 'PSRule for Azure' CSV '$PSRuleCSVPath' still exceeds the GitHub file limit of 100MB" + Write-Host " Re-Exporting 'PSRule for Azure' CSV '$PSRuleCSVPath' excluding column 'description', 'recommendation'" + $arrayPsRule | Select-Object -ExcludeProperty description, recommendation | Sort-Object -Property resourceId, pillar, category, severity, rule | Export-Csv -Path "$PSRuleCSVPath" -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $exportCSVPSRuleFileSize = (Get-Item -Path $PSRuleCSVPath).length / 1MB + if ($exportCSVPSRuleFileSize -gt 100) { + Write-Host " The exported 'PSRule for Azure' CSV '$PSRuleCSVPath' still exceeds the GitHub file limit of 100MB" + Write-Host " Deleting 'PSRule for Azure' CSV '$PSRuleCSVPath' in order to prevent the workflow from failing at push to repo" + Remove-Item -Path $PSRuleCSVPath + } + } + else { + Write-Host " Info: The exported 'PSRule for Azure' CSV '$PSRuleCSVPath' does not exceed the GitHub file limit of 100MB" + } + } + } + + $grpPSRuleAll = $arrayPsRule | Group-Object -Property resourceType, pillar, category, severity, rule, result + $tfCount = $grpPSRuleAll.Name.Count + + $htmlTableId = 'TenantSummary_PSRule' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Learn about PSRule for Azure
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $grpPSRuleAll | Sort-Object -Property Name) { + $resultNameSplit = $result.Name.split(', ') + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Resource TypeResource CountSubscription CountPillarCategorySeverityRuleRecommendationlnkState
$($resultNameSplit[0])$($result.Group.Count)$(($result.Group.subscriptionId | Sort-Object -Unique).Count)$($resultNameSplit[1])$($resultNameSplit[2])$($resultNameSplit[3])$(($result.Group[0].rule))$(($result.Group[0].recommendation))$($resultNameSplit[5])
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No PSRule for Azure results

+'@) + } + $endPSRule = Get-Date + Write-Host " PSRule for Azure processing duration: $((New-TimeSpan -Start $startPSRule -End $endPSRule).TotalMinutes) minutes ($((New-TimeSpan -Start $startPSRule -End $endPSRule).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@' + PSRule for Azure - integration paused - PSRule for Azure +'@) + } + #endregion SUMMARYPSRule + } + + #region SUMMARYStorageAccountAnalysis + if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) { + $startStorageAccountAnalysis = Get-Date + Write-Host ' processing TenantSummary Storage Account Access Analysis' + + $arrayStorageAccountAnalysisResultsCount = $arrayStorageAccountAnalysisResults.Count + if ($arrayStorageAccountAnalysisResultsCount -gt 0) { + + if (-not $NoCsvExport) { + $storageAccountAccessAnalysisCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_StorageAccountAccessAnalysis.csv" + Write-Host " Exporting 'Storage Account Access Analysis' CSV '$storageAccountAccessAnalysisCSVPath'" + $arrayStorageAccountAnalysisResults | Sort-Object -Property StorageAccount | Export-Csv -Path $storageAccountAccessAnalysisCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + $saAnonymousAccessCount = ($arrayStorageAccountAnalysisResults.where({ $_.containersAnonymousContainerCount -gt 0 -or $_.containersAnonymousBlobCount -gt 0 })).Count + $saStaticWebsitesEnabledCount = ($arrayStorageAccountAnalysisResults.where({ $_.staticWebsitesState -eq $true })).Count + + $htmlTableId = 'TenantSummary_StorageAccountAccessAnalysis' + $tfCount = $arrayStorageAccountAnalysisResultsCount + + if ($DoAzureConsumption -eq $true) { + $costDays = " ($($AzureConsumptionPeriod)d)" + } + else { + $costDays = " (-DoAzureConsumption = $DoAzureConsumption)" + } + + [void]$htmlTenantSummary.AppendLine(@" + +
+ Check this article by Elli Shlomo (MVP) Azure Blob Container Threats & Attacks
+ If you enabled the parameters StorageAccountAccessAnalysisSubscriptionTags or StorageAccountAccessAnalysisStorageAccountTags these are integrated in the CSV output *_StorageAccountAccessAnalysis.csv
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $arrayStorageAccountAnalysisResults | Sort-Object -Property storageAccount) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
StorageAccountKindSkuNameSkuTierLocationSubscriptionSubscription MGPathResourceGroupAllow Blob Public AccessPublic Network AccessNetworkAcls defaultActionStaticWebsites StateStaticWebsites ResponseContainers CanBeListedContainers CountContainers Anonymous Container CountContainers Anonymous Blob CountIpRules CountIpRules IPAddress ListVirtualNetwork Rules CountResourceAccess Rules CountResourceAccess RulesBypassSupports Https Traffic OnlyMinimum Tls VersionAllow SharedKey AccessRequire Infrastructure EncryptionAllowed Copy ScopeAllow Cross Tenant ReplicationDNS Endpoint TypeUsed Capacity (GB)Cost$costDaysCurrencyCost categories
$($result.storageAccount)$($result.kind)$($result.skuName)$($result.skuTier)$($result.location)$($result.SubscriptionName)$($result.subscriptionMGPath)$($result.resourceGroup)$($result.allowBlobPublicAccess)$($result.publicNetworkAccess)$($result.networkAclsdefaultAction)$($result.staticWebsitesState)$($result.staticWebsitesResponse)$($result.containersCanBeListed)$($result.containersCount)$($result.containersAnonymousContainerCount)$($result.containersAnonymousBlobCount)$($result.ipRulesCount)$($result.ipRulesIPAddressList)$($result.virtualNetworkRulesCount)$($result.resourceAccessRulesCount)$($result.resourceAccessRules)$($result.bypass)$($result.supportsHttpsTrafficOnly)$($result.minimumTlsVersion)$($result.allowSharedKeyAccess)$($result.requireInfrastructureEncryption)$($result.allowedCopyScope)$($result.allowCrossTenantReplication)$($result.dnsEndpointType)$($result.usedCapacity)$($result.cost)$($result.curreny)$($result.metercategory)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Storage Accounts found

+'@) + } + $endStorageAccountAnalysis = Get-Date + Write-Host " Storage Account Analysis processing duration: $((New-TimeSpan -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalMinutes) minutes ($((New-TimeSpan -Start $startStorageAccountAnalysis -End $endStorageAccountAnalysis).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Storage Account Access Analysis disabled - parameter -NoStorageAccountAccessAnalysis = $($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis)

+"@) + } + #endregion SUMMARYStorageAccountAnalysis + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummarySubscriptionsResourceDefenderPSRule + + #region tenantSummaryNetwork + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + #region SUMMARYVNets + if ($azAPICallConf['htParameters'].NoNetwork -eq $false) { + $startVNets = Get-Date + Write-Host ' processing TenantSummary VNets' + $Vnets = $arrayVirtualNetworks | Sort-Object -Property SubscriptionName, VNet, VNetId -Unique | Select-Object SubscriptionName, Subscription, MGPath, VNet, VNetResourceGroup, Location, AddressSpaceAddressPrefixes, DhcpoptionsDnsservers, SubnetsCount, SubnetsWithNSGCount, SubnetsWithRouteTableCount, SubnetsWithDelegationsCount, PrivateEndpointsCount, SubnetsWithPrivateEndPointsCount, ConnectedDevices, SubnetsWithConnectedDevicesCount, DdosProtection, PeeringsCount + $VNetsCount = $Vnets.Count + + if (-not $NoCsvExport) { + $virtualNetworksCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_VirtualNetworks.csv" + Write-Host " Exporting VirtaulNetworks CSV '$virtualNetworksCSVPath'" + $Vnets | Export-Csv -Path $virtualNetworksCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + if ($VNetsCount -gt 0) { + + $htmlTableId = 'TenantSummary_VNets' + $tfCount = $VNetsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $Vnets) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Subscription NameSubscriptionMGPathVNetVNet Resource GroupLocationAddress PrefixesDNS ServersSubnetsSubnets with NSGSubnets with RouteTableSubnets with DelegationPrivate EndpointsSubnets with Private EndpointsConnected deviceSubnets with connected deviceDDoSPeerings Count
$($result.SubscriptionName)$($result.Subscription)$($result.MGPath)$($result.VNet)$($result.VNetResourceGroup)$($result.Location)$($result.AddressSpaceAddressPrefixes)$($result.DhcpoptionsDnsservers)$($result.SubnetsCount)$($result.SubnetsWithNSGCount)$($result.SubnetsWithRouteTableCount)$($result.SubnetsWithDelegationsCount)$($result.PrivateEndpointsCount)$($result.SubnetsWithPrivateEndPointsCount)$($result.ConnectedDevices)$($result.SubnetsWithConnectedDevicesCount)$($result.DdosProtection)$($result.PeeringsCount)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Virtual Networks

+'@) + } + $endVNets = Get-Date + Write-Host " VNets processing duration: $((New-TimeSpan -Start $startVNets -End $endVNets).TotalMinutes) minutes ($((New-TimeSpan -Start $startVNets -End $endVNets).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Virtual Networks - Network Analysis disabled - parameter -NoNetwork = $($azAPICallConf['htParameters'].NoNetwork)

+"@) + } + #endregion SUMMARYVNets + + #region SUMMARYSubnets + if ($azAPICallConf['htParameters'].NoNetwork -eq $false) { + $startSubnets = Get-Date + Write-Host ' processing TenantSummary Subnets' + $subnets = $arraySubnets | Sort-Object -Property SubscriptionName, VNet, VNetId, SubnetName + $subnetsCount = $subnets.Count + + if (-not $NoCsvExport) { + $subnetsCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_VirtualNetworkSubnets.csv" + Write-Host " Exporting Subnets CSV '$subnetsCSVPath'" + $subnets | Export-Csv -Path $subnetsCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + if ($subnetsCount -gt 0) { + + $subnetIPAddressUsageCriticalCount = ($subnets.where({ $_.SubnetIPAddressUsageCritical -eq $true })).Count + $criticalUsageText = '' + if ($subnetIPAddressUsageCriticalCount -gt 0) { + $criticalUsageText = " ($subnetIPAddressUsageCriticalCount > $($NetworkSubnetIPAddressUsageCriticalPercentage)% IP addresses used)" + } + + $htmlTableId = 'TenantSummary_Subnets' + $tfCount = $subnetsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $subnets) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Subscription NameSubscriptionMGPathVNetVNet Resource GroupLocationNameIdSubnetPrefixMaskRangeConnected devicesFree IP addressesUsed IP addresses %Private Endpoint Network PoliciesPrivate Link Service Network PoliciesService Endpoints countService EndpointsDelegationNSGRoute TableNat GatewayPrivate Endpoints
$($result.SubscriptionName)$($result.Subscription)$($result.MGPath)$($result.VNet)$($result.VNetResourceGroup)$($result.Location)$($result.SubnetName)$($result.SubnetId)$($result.SubnetNet)$($result.SubnetPrefix)$($result.Subnetmask)$($result.Range)$($result.ConnectedDevices)$($result.AvailableIPAddresses)$($result.UsedIPAddressesPercent)$($result.PrivateEndpointNetworkPolicies)$($result.PrivateLinkServiceNetworkPolicies)$($result.ServiceEndpointsCount)$($result.ServiceEndpoints)$($result.Delegation)$($result.NetworkSecurityGroup)$($result.RouteTable)$($result.NatGateway)$($result.PrivateEndpoints)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Subnets

+'@) + } + $endSubnets = Get-Date + Write-Host " Subnets processing duration: $((New-TimeSpan -Start $startSubnets -End $endSubnets).TotalMinutes) minutes ($((New-TimeSpan -Start $startSubnets -End $endSubnets).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Subnets - Network Analysis disabled - parameter -NoNetwork = $($azAPICallConf['htParameters'].NoNetwork)

+"@) + } + #endregion SUMMARYSubnets + + #region SUMMARYVNetPeerings + if ($azAPICallConf['htParameters'].NoNetwork -eq $false) { + $startVNetPeerings = Get-Date + Write-Host ' processing TenantSummary VNet Peerings' + $vnetPeerings = $arrayVirtualNetworks.where({ $_.PeeringsCount -gt 0 }) | Sort-Object -Property SubscriptionName, VNet, VNetId + $VNetsPeeringsCount = $vnetPeerings.Count + + if (-not $NoCsvExport) { + $virtualNetworkPeeringsCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_VirtualNetworkPeerings.csv" + Write-Host " Exporting VirtaulNetworks CSV '$virtualNetworkPeeringsCSVPath'" + $vnetPeerings | Export-Csv -Path $virtualNetworkPeeringsCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + if ($VNetsPeeringsCount -gt 0) { + $vnetPeeringsGroupedByPeeringState = $vnetPeerings | Group-Object -Property PeeringState + $arrayPeeringState = foreach ($peeringState in $vnetPeeringsGroupedByPeeringState) { + "$($peeringState.Name): $($peeringState.Count)" + } + + $xTenantPeeringsCount = $vnetPeerings.where({ $_.PeeringXTenant -eq 'true' }).Count + + $htmlTableId = 'TenantSummary_VNetPeerings' + $tfCount = $VNetsPeeringsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $vnetPeerings) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
Subscription NameSubscriptionMGPathVNetVNet Resource GroupLocationAddress PrefixesDNS ServersSubnetsSubnets with NSGSubnets with RouteTableSubnets with DelegationPrivate EndpointsSubnets with Private EndpointsConnected deviceSubnets with connected deviceDDoSPeerings CountPeering Cross TenantPeering NamePeering StatePeering Sync LevelAllow Virtual Network AccessAllow Forwarded TrafficAllow Gateway TransitUse Remote GatewaysDo Not Verify Remote GatewaysPeer Complete VnetsRoute Service VipsRemote Peerings CountRemote Peering NameRemote Peering StateRemote Peering Sync LevelRemote Allow Virtual Network AccessRemote Allow Forwarded TrafficRemote Allow Gateway TransitRemote Use Remote GatewaysRemote Do Not Verify Remote GatewaysRemote Peer Complete VnetsRemote Route Service VipsRemote Subscription NameRemote SubscriptionRemote MGPathRemote VNetRemote VNet StateRemote VNet Resource GroupRemote LocationRemote Address Space Address PrefixesRemote Virtual Network AddressSpace Address PrefixesRemote DNS ServersRemote SubnetsRemote Subnets with NSGRemote Subnets with RouteTableRemote Subnets with DelegationRemote Private EndpointsRemote Subnets with Private EndpointsRemote Connected devicesRemote Subnets with connected devicesRemote DDoS
$($result.SubscriptionName)$($result.Subscription)$($result.MGPath)$($result.VNet)$($result.VNetResourceGroup)$($result.Location)$($result.AddressSpaceAddressPrefixes)$($result.DhcpoptionsDnsservers)$($result.SubnetsCount)$($result.SubnetsWithNSGCount)$($result.SubnetsWithRouteTableCount)$($result.SubnetsWithDelegationsCount)$($result.PrivateEndpointsCount)$($result.SubnetsWithPrivateEndPointsCount)$($result.ConnectedDevices)$($result.SubnetsWithConnectedDevicesCount)$($result.DdosProtection)$($result.PeeringsCount)$($result.PeeringXTenant)$($result.PeeringName)$($result.PeeringState)$($result.PeeringSyncLevel)$($result.AllowVirtualNetworkAccess)$($result.AllowForwardedTraffic)$($result.AllowGatewayTransit)$($result.UseRemoteGateways)$($result.DoNotVerifyRemoteGateways)$($result.PeerCompleteVnets)$($result.RouteServiceVips)$($result.RemotePeeringsCount)$($result.RemotePeeringName)$($result.RemotePeeringState)$($result.RemotePeeringSyncLevel)$($result.RemoteAllowVirtualNetworkAccess)$($result.RemoteAllowForwardedTraffic)$($result.RemoteAllowGatewayTransit)$($result.RemoteUseRemoteGateways)$($result.RemoteDoNotVerifyRemoteGateways)$($result.RemotePeerCompleteVnets)$($result.RemoteRouteServiceVips)$($result.RemoteSubscriptionName)$($result.RemoteSubscription)$($result.RemoteMGPath)$($result.RemoteVNet)$($result.RemoteVNetState)$($result.RemoteVNetResourceGroup)$($result.RemoteVNetLocation)$($result.RemoteAddressSpaceAddressPrefixes)$($result.RemoteVirtualNetworkAddressSpaceAddressPrefixes)$($result.RemoteDhcpoptionsDnsservers)$($result.RemoteSubnetsCount)$($result.RemoteSubnetsWithNSGCount)$($result.RemoteSubnetsWithRouteTable)$($result.RemoteSubnetsWithDelegations)$($result.RemotePrivateEndPoints)$($result.RemoteSubnetsWithPrivateEndPoints)$($result.RemoteConnectedDevices)$($result.RemoteSubnetsWithConnectedDevices)$($result.RemoteDdosProtection)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Virtual Network Peerings

+'@) + } + $endVNetPeerings = Get-Date + Write-Host " VNet Peerings processing duration: $((New-TimeSpan -Start $startVNetPeerings -End $endVNetPeerings).TotalMinutes) minutes ($((New-TimeSpan -Start $startVNetPeerings -End $endVNetPeerings).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Virtual Network Peerings - Network Analysis disabled - parameter -NoNetwork = $($azAPICallConf['htParameters'].NoNetwork)

+"@) + } + #endregion SUMMARYVNetPeerings + + #region SUMMARYPrivateEndpoints + if ($azAPICallConf['htParameters'].NoNetwork -eq $false) { + $startPrivateEndpoints = Get-Date + Write-Host ' processing TenantSummary PrivateEndpoints' + $privateEndPoints = $arrayPrivateEndpointsEnriched | Sort-Object -Property PESubscriptionName, PEName + $privateEndPointsCount = $privateEndPoints.Count + + if (-not $NoCsvExport) { + $peCSVPath = "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PrivateEndpoints.csv" + Write-Host " Exporting PrivateEndpoints CSV '$peCSVPath'" + $privateEndPoints | Export-Csv -Path $peCSVPath -Delimiter "$csvDelimiter" -NoTypeInformation + } + + if ($privateEndPointsCount -gt 0) { + + $crossSubPECount = ($privateEndPoints.where({ $_.crossSubscriptionPE -eq $true })).Count + $crossSubPEText = '' + if ($crossSubPECount -gt 0) { + $crossSubPEText = " ($crossSubPECount cross Subscription)" + } + $crossTenantPECount = ($privateEndPoints.where({ $_.crossTenantPE -eq $true })).Count + $crossTenantPEText = '' + if ($crossTenantPECount -gt 0) { + $crossTenantPEText = " ($crossTenantPECount cross Tenant)" + } + + $htmlTableId = 'TenantSummary_PrivateEndpoints' + $tfCount = $privateEndPointsCount + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($result in $privateEndPoints) { + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + + } + + [void]$htmlTenantSummary.AppendLine(@" + +
PE NamePE IdPE LocationPE Resource GroupPE Subscription NamePE SubscriptionPE MGPathPE TypePE StateCross Subscription PECross Tenant PEResourceResource TypeResource IdTarget SubresourceNIC NameFQDNIP addressesResource Resource GroupResource Subscription NameResource Subscription IdResource MGPathResource Cross TenantSubnetSubnet IdVNetVNet IdVNet LocationVNet Resource GroupSubnet Subscription NameSubnet Subscription IdSubnet MGPath
$($result.PEName)$($result.PEId)$($result.PELocation)$($result.PEResourceGroup)$($result.PESubscriptionName)$($result.PESubscription)$($result.PEMGPath)$($result.PEConnectionType)$($result.PEConnectionState)$($result.CrossSubscriptionPE)$($result.CrossTenantPE)$($result.Resource)$($result.ResourceType)$($result.ResourceId)$($result.TargetSubresource)$($result.NICName)$($result.FQDN)$($result.ipAddresses)$($result.ResourceResourceGroup)$($result.ResourceSubscriptionName)$($result.ResourceSubscriptionId)$($result.ResourceMGPath)$($result.ResourceCrossTenant)$($result.Subnet)$($result.SubnetId)$($result.SubnetVNet)$($result.SubnetVNetId)$($result.SubnetVNetLocation)$($result.SubnetVNetResourceGroup)$($result.SubnetSubscriptionName)$($result.SubnetSubscription)$($result.SubnetMGPath)
+ +
+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Private Endpoints

+'@) + } + $endPrivateEndpoints = Get-Date + Write-Host " PrivateEndpoints processing duration: $((New-TimeSpan -Start $startPrivateEndpoints -End $endPrivateEndpoints).TotalMinutes) minutes ($((New-TimeSpan -Start $startPrivateEndpoints -End $endPrivateEndpoints).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Private Endpoints - Network Analysis disabled - parameter -NoNetwork = $($azAPICallConf['htParameters'].NoNetwork)

+"@) + } + #endregion SUMMARYPrivateEndpoints + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryNetwork + + showMemoryUsage + + #region tenantSummaryDiagnostics + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + [void]$htmlTenantSummary.AppendLine( @' +

Management Groups

+'@) + + #region SUMMARYDiagnosticsManagementGroups + Write-Host ' processing TenantSummary Diagnostics Management Groups' + + #hasDiag + if ($diagnosticSettingsMgCount -gt 0) { + $tfCount = $diagnosticSettingsMgCount + $htmlTableId = 'TenantSummary_DiagnosticsManagementGroups' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Download CSV semicolon | comma + + + + + + + + + + +"@) + + foreach ($logCategory in $diagnosticSettingsMgCategories) { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + + [void]$htmlTenantSummary.AppendLine(@' + + + +'@) + $htmlSUMMARYDiagnosticsManagementGroups = $null + $htmlSUMMARYDiagnosticsManagementGroups = foreach ($entry in $diagnosticSettingsMg | Sort-Object -Property ScopeMgPath, DiagnosticsInheritedFrom, DiagnosticSettingName, DiagnosticTargetType, DiagnosticTargetId) { + + @" + + + + + + + + +"@ + foreach ($logCategory in $diagnosticSettingsMgCategories) { + if ($entry.DiagnosticCategoriesHt.($logCategory)) { + @" + +"@ + } + else { + @' + +'@ + } + } + @' + +'@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYDiagnosticsManagementGroups) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdDiagnostic settingInheritanceInherited fromTargetTargetId$logCategory
$($entry.ScopeName -replace '<', '<' -replace '>', '>')$($entry.ScopeId)$($entry.DiagnosticSettingName)$($entry.DiagnosticsInheritedOrnot)$($entry.DiagnosticsInheritedFrom)$($entry.DiagnosticTargetType)$($entry.DiagnosticTargetId)$($entry.DiagnosticCategoriesHt.($logCategory))n/a
+
+ +"@) + } + else { + + [void]$htmlTenantSummary.AppendLine(@' +

No Management Groups configured for Diagnostic settings docs

+'@) + } + + #hasNoDiag + if ($arrayMgsWithoutDiagnosticsCount -gt 0) { + $tfCount = $arrayMgsWithoutDiagnosticsCount + $htmlTableId = 'TenantSummary_NoDiagnosticsManagementGroups' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Management Group Diagnostic Settings - Create Or Update - REST API docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYNoDiagnosticsManagementGroups = $null + $htmlSUMMARYNoDiagnosticsManagementGroups = foreach ($entry in $arrayMgsWithoutDiagnostics | Sort-Object -Property ScopeMgPath) { + + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNoDiagnosticsManagementGroups) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdManagement Group path
$($entry.ScopeName -replace '<', '<' -replace '>', '>')$($entry.ScopeId)$($entry.ScopeMgPath)
+
+ +"@) + } + else { + + [void]$htmlTenantSummary.AppendLine(@' +

All Management Groups are configured for Diagnostic settings docs

+'@) + } + #endregion SUMMARYDiagnosticsManagementGroups + + #region subscriptions + [void]$htmlTenantSummary.AppendLine( @' +

Subscriptions

+'@) + + #region SUMMARYDiagnosticsSubscriptions + Write-Host ' processing TenantSummary Diagnostics Subscriptions' + + #hasDiag + if ($diagnosticSettingsSubCount -gt 0) { + $tfCount = $diagnosticSettingsSubCount + $htmlTableId = 'TenantSummary_DiagnosticsSubscriptions' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Create diagnostic setting docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + + foreach ($logCategory in $diagnosticSettingsSubCategories) { + [void]$htmlTenantSummary.AppendLine(@" + +"@) + } + + [void]$htmlTenantSummary.AppendLine(@' + + + +'@) + $htmlSUMMARYDiagnosticsSubscriptions = $null + $htmlSUMMARYDiagnosticsSubscriptions = foreach ($entry in $diagnosticSettingsSub | Sort-Object -Property ScopeName, DiagnosticTargetType, DiagnosticSettingName) { + + @" + + + + + + + +"@ + foreach ($logCategory in $diagnosticSettingsSubCategories) { + if ($entry.DiagnosticCategoriesHt.($logCategory)) { + @" + +"@ + } + else { + @' + +'@ + } + } + @' + +'@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYDiagnosticsSubscriptions) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdPathDiagnostic settingTargetTargetId$logCategory
$($entry.ScopeName -replace '<', '<' -replace '>', '>')$($entry.ScopeId) $($entry.ScopeMgPath)$($entry.DiagnosticSettingName)$($entry.DiagnosticTargetType)$($entry.DiagnosticTargetId)$($entry.DiagnosticCategoriesHt.($logCategory))n/a
+
+ +"@) + } + else { + + [void]$htmlTenantSummary.AppendLine(@' +

No Subscriptions configured for Diagnostic settings docs

+'@) + } + + #hasNoDiag + if ($diagnosticSettingsSubNoDiagCount -gt 0) { + $tfCount = $diagnosticSettingsSubNoDiagCount + $htmlTableId = 'TenantSummary_NoDiagnosticsSubscriptions' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Create diagnostic setting docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYNoDiagnosticsSubscriptions = $null + $htmlSUMMARYNoDiagnosticsSubscriptions = foreach ($entry in $diagnosticSettingsSubNoDiag | Sort-Object -Property ScopeMgPath) { + + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNoDiagnosticsSubscriptions) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscription IdSubscription Mg path
$($entry.ScopeName -replace '<', '<' -replace '>', '>')$($entry.ScopeId)$($entry.ScopeMgPath)
+
+ +"@) + } + else { + + [void]$htmlTenantSummary.AppendLine(@' +

All Subscriptions are configured for Diagnostic settings docs

+'@) + } + #endregion SUMMARYDiagnosticsSubscriptions + + #endregion subscriptions + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region resources + [void]$htmlTenantSummary.AppendLine( @' +

Resources

+'@) + + #region SUMMARYResourcesDiagnosticsCapable + Write-Host ' processing TenantSummary Diagnostics Resources Diagnostics Capable (1st party only)' + $resourceTypesDiagnosticsArraySorted = $resourceTypesDiagnosticsArray | Sort-Object -Property ResourceType, ResourceCount, Metrics, Logs, LogCategories + $resourceTypesDiagnosticsArraySortedCount = ($resourceTypesDiagnosticsArraySorted | Measure-Object).count + $resourceTypesDiagnosticsMetricsTrueCount = ($resourceTypesDiagnosticsArray.where( { $_.Metrics -eq $True }) | Measure-Object).count + $resourceTypesDiagnosticsLogsTrueCount = ($resourceTypesDiagnosticsArray.where( { $_.Logs -eq $True }) | Measure-Object).count + $resourceTypesDiagnosticsMetricsLogsTrueCount = ($resourceTypesDiagnosticsArray.where( { $_.Metrics -eq $True -or $_.Logs -eq $True }) | Measure-Object).count + if ($resourceTypesDiagnosticsArraySortedCount -gt 0) { + $tfCount = $resourceTypesDiagnosticsArraySortedCount + $htmlTableId = 'TenantSummary_ResourcesDiagnosticsCapable' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
+ Supported categories for Azure Resource Logs docs
+ Download CSV semicolon | comma + + + + + + + + + + + + +"@) + $htmlSUMMARYResourcesDiagnosticsCapable = $null + $htmlSUMMARYResourcesDiagnosticsCapable = foreach ($resourceType in $resourceTypesDiagnosticsArraySorted) { + if ($resourceType.Metrics -eq $true -or $resourceType.Logs -eq $true) { + $diagnosticsCapable = $true + } + else { + if ($resourceType.Metrics -eq 'n/a - resourcesMeanwhileDeleted' -or $resourceType.Logs -eq 'n/a - resourcesMeanwhileDeleted') { + $diagnosticsCapable = 'n/a' + } + else { + $diagnosticsCapable = $false + } + } + @" + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYResourcesDiagnosticsCapable) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeResource CountDiagnostics capableMetricsLogsLogCategories
$($resourceType.ResourceType)$($resourceType.ResourceCount)$diagnosticsCapable$($resourceType.Metrics)$($resourceType.Logs)$($resourceType.LogCategories -join "$CsvDelimiterOpposite ")
+
+ +"@) + } + else { + + [void]$htmlTenantSummary.AppendLine(@' +

No Resources (1st party) Diagnostics capable

+'@) + } + #endregion SUMMARYResourcesDiagnosticsCapable + + #region SUMMARYDiagnosticsPolicyLifecycle + if (-not $NoResourceDiagnosticsPolicyLifecycle) { + Write-Host ' processing TenantSummary Diagnostics Resource Diagnostics Policy Lifecycle' + $startsumDiagLifecycle = Get-Date + + if ($tenantCustomPoliciesCount -gt 0) { + $policiesThatDefineDiagnostics = $tenantCustomPolicies.where( { $_.Type -eq 'custom' -and $_.Json.properties.policyrule.then.details.type -eq 'Microsoft.Insights/diagnosticSettings' -and $_.Json.properties.policyrule.then.details.deployment.properties.template.resources.type -match '/providers/diagnosticSettings' } ) + + $policiesThatDefineDiagnosticsCount = ($policiesThatDefineDiagnostics).count + if ($policiesThatDefineDiagnosticsCount -gt 0) { + + $diagnosticsPolicyAnalysis = @() + $diagnosticsPolicyAnalysis = [System.Collections.ArrayList]@() + foreach ($policy in $policiesThatDefineDiagnostics) { + + if ( + (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.workspaceId -or + (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.eventHubAuthorizationRuleId -or + (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.storageAccountId + ) { + if ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.workspaceId) { + $diagnosticsDestination = 'LA' + } + if ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.eventHubAuthorizationRuleId) { + $diagnosticsDestination = 'EH' + } + if ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.storageAccountId) { + $diagnosticsDestination = 'SA' + } + + if ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.logs ) { + + $resourceType = ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).type -replace '/providers/diagnosticSettings') + + $resourceTypeCountFromResourceTypesSummarizedArray = ($resourceTypesSummarizedArray.where( { $_.ResourceType -eq $resourceType })).ResourceCount + if ($resourceTypeCountFromResourceTypesSummarizedArray) { + $resourceCount = $resourceTypeCountFromResourceTypesSummarizedArray + } + else { + $resourceCount = '0' + } + $supportedLogs = $resourceTypesDiagnosticsArray | Where-Object { $_.ResourceType -eq ( (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).type -replace '/providers/diagnosticSettings') } + + $diagnosticsLogCategoriesSupported = $supportedLogs.LogCategories + if (($supportedLogs | Measure-Object).count -gt 0) { + $logsSupported = 'yes' + } + else { + $logsSupported = 'no' + } + + $roleDefinitionIdsArray = [System.Collections.ArrayList]@() + foreach ($roleDefinitionId in ($policy).Json.properties.policyrule.then.details.roleDefinitionIds) { + if (($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/')) { + $null = $roleDefinitionIdsArray.Add("$(($htCacheDefinitionsRole).($roleDefinitionId -replace '.*/').Name) ($($roleDefinitionId -replace '.*/'))") + } + else { + Write-Host " DiagnosticsLifeCycle: unknown RoleDefinition '$roleDefinitionId'" + $null = $roleDefinitionIdsArray.Add("unknown RoleDefinition: '$roleDefinitionId'") + } + } + + $policyHasPolicyAssignments = $policyBaseQuery | Where-Object { $_.PolicyDefinitionId -eq $policy.Id } | Sort-Object -Property PolicyDefinitionId, PolicyAssignmentId -Unique + $policyHasPolicyAssignmentCount = ($policyHasPolicyAssignments | Measure-Object).count + if ($policyHasPolicyAssignmentCount -gt 0) { + $policyAssignmentsArray = @() + $policyAssignmentsArray += foreach ($policyAssignment in $policyHasPolicyAssignments) { + "$($policyAssignment.PolicyAssignmentId) ($($policyAssignment.PolicyAssignmentDisplayName))" + } + $policyAssignmentsCollCount = ($policyAssignmentsArray).count + $policyAssignmentsColl = $policyAssignmentsCollCount + } + else { + $policyAssignmentsColl = 0 + } + + #PolicyUsedinPolicySet + $policySetAssignmentsColl = 0 + $policySetAssignmentsArray = @() + $policyUsedinPolicySets = 'n/a' + + $usedInPolicySetArray = [System.Collections.ArrayList]@() + foreach ($customPolicySet in $tenantCustomPolicySets) { + if ($customPolicySet.Type -eq 'Custom') { + $hlpCustomPolicySet = ($customPolicySet) + if (($hlpCustomPolicySet.PolicySetPolicyIds) -contains ($policy.Id)) { + $null = $usedInPolicySetArray.Add("$($hlpCustomPolicySet.Id) ($($hlpCustomPolicySet.DisplayName))") + + #PolicySetHasAssignments + $policySetAssignments = ($htCacheAssignmentsPolicy).Values.where( { $_.Assignment.properties.policyDefinitionId -eq ($hlpCustomPolicySet.Id) } ) + $policySetAssignmentsCount = ($policySetAssignments).count + if ($policySetAssignmentsCount -gt 0) { + $policySetAssignmentsArray += foreach ($policySetAssignment in $policySetAssignments) { + "$(($policySetAssignment.Assignment.id).Tolower()) ($($policySetAssignment.Assignment.properties.displayName))" + } + $policySetAssignmentsCollCount = ($policySetAssignmentsArray).Count + $policySetAssignmentsColl = "$policySetAssignmentsCollCount [$($policySetAssignmentsArray -join "$CsvDelimiterOpposite ")]" + } + + } + } + } + + if (($usedInPolicySetArray | Measure-Object).count -gt 0) { + $policyUsedinPolicySets = "$(($usedInPolicySetArray | Measure-Object).count) [$($usedInPolicySetArray -join "$CsvDelimiterOpposite ")]" + } + else { + $policyUsedinPolicySets = "$(($usedInPolicySetArray | Measure-Object).count)" + } + + if ($recommendation -eq 'review the policy and add the missing categories as required') { + if ($policyAssignmentsColl -gt 0 -or $policySetAssignmentsColl -gt 0) { + $priority = '1-High' + } + else { + $priority = '3-MediumLow' + } + } + else { + $priority = '4-Low' + } + + $diagnosticsLogCategoriesCoveredByPolicy = (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.logs + if (($diagnosticsLogCategoriesCoveredByPolicy.category | Measure-Object).count -gt 0) { + + if (($supportedLogs | Measure-Object).count -gt 0) { + $actionItems = @() + $actionItems += foreach ($supportedLogCategory in $supportedLogs.LogCategories) { + if ($diagnosticsLogCategoriesCoveredByPolicy.category -notcontains ($supportedLogCategory)) { + $supportedLogCategory + } + } + if (($actionItems | Measure-Object).count -gt 0) { + $diagnosticsLogCategoriesNotCoveredByPolicy = $actionItems + $recommendation = 'review the policy and add the missing categories as required' + } + else { + $diagnosticsLogCategoriesNotCoveredByPolicy = 'all OK' + $recommendation = 'no recommendation' + } + } + else { + $status = 'Azure Governance Visualizer did not detect the resourceType' + $diagnosticsLogCategoriesSupported = 'n/a' + $diagnosticsLogCategoriesNotCoveredByPolicy = 'n/a' + $recommendation = 'no recommendation as this resourceType seems not existing' + $logsSupported = 'unknown' + } + + $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ + Priority = $priority + PolicyId = ($policy).Id + PolicyCategory = ($policy).Category + PolicyName = ($policy).DisplayName + PolicyDeploysRoles = $roleDefinitionIdsArray -join "$CsvDelimiterOpposite " + PolicyForResourceTypeExists = $true + ResourceType = $resourceType + ResourceTypeCount = $resourceCount + Status = $status + LogsSupported = $logsSupported + LogCategoriesInPolicy = ($diagnosticsLogCategoriesCoveredByPolicy.category | Sort-Object) -join "$CsvDelimiterOpposite " + LogCategoriesSupported = ($diagnosticsLogCategoriesSupported | Sort-Object) -join "$CsvDelimiterOpposite " + LogCategoriesDelta = ($diagnosticsLogCategoriesNotCoveredByPolicy | Sort-Object) -join "$CsvDelimiterOpposite " + Recommendation = $recommendation + DiagnosticsTargetType = $diagnosticsDestination + PolicyAssignments = $policyAssignmentsColl + PolicyUsedInPolicySet = $policyUsedinPolicySets + PolicySetAssignments = $policySetAssignmentsColl + }) + + } + else { + $status = 'no categories defined' + $priority = '5-Low' + $recommendation = 'Review the policy - the definition has key for categories, but there are none categories defined' + $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ + Priority = $priority + PolicyId = ($policy).Id + PolicyCategory = ($policy).Category + PolicyName = ($policy).DisplayName + PolicyDeploysRoles = $roleDefinitionIdsArray -join "$CsvDelimiterOpposite " + PolicyForResourceTypeExists = $true + ResourceType = $resourceType + ResourceTypeCount = $resourceCount + Status = $status + LogsSupported = $logsSupported + LogCategoriesInPolicy = 'none' + LogCategoriesSupported = ($diagnosticsLogCategoriesSupported | Sort-Object) -join "$CsvDelimiterOpposite " + LogCategoriesDelta = ($diagnosticsLogCategoriesSupported | Sort-Object) -join "$CsvDelimiterOpposite " + Recommendation = $recommendation + DiagnosticsTargetType = $diagnosticsDestination + PolicyAssignments = $policyAssignmentsColl + PolicyUsedInPolicySet = $policyUsedinPolicySets + PolicySetAssignments = $policySetAssignmentsColl + }) + } + } + else { + if (-not (($policy).Json.properties.policyrule.then.details.deployment.properties.template.resources | Where-Object { $_.type -match '/providers/diagnosticSettings' }).properties.metrics ) { + Write-Host " DiagnosticsLifeCycle check?!: $($policy.DisplayName) ($($policy.Id)) - something unexpected, no Logs and no Metrics defined" + } + } + } + else { + Write-Host " DiagnosticsLifeCycle check?!: $($policy.DisplayName) ($($policy.Id)) - something unexpected - not EH, LA, SA" + } + } + #where no Policy exists + $diagnosticsPolicyAnalysisCount = ($diagnosticsPolicyAnalysis).count + if ($diagnosticsPolicyAnalysisCount -gt 0) { + foreach ($resourceTypeDiagnosticsCapable in $resourceTypesDiagnosticsArray | Where-Object { $_.Logs -eq $true }) { + if (($diagnosticsPolicyAnalysis.ResourceType).ToLower() -notcontains ( ($resourceTypeDiagnosticsCapable.ResourceType).ToLower() )) { + $supportedLogs = ($resourceTypesDiagnosticsArray | Where-Object { $_.ResourceType -eq $resourceTypeDiagnosticsCapable.ResourceType }).LogCategories + $logsSupported = 'yes' + $resourceTypeCountFromResourceTypesSummarizedArray = ($resourceTypesSummarizedArray | Where-Object { $_.ResourceType -eq $resourceTypeDiagnosticsCapable.ResourceType }).ResourceCount + if ($resourceTypeCountFromResourceTypesSummarizedArray) { + $resourceCount = $resourceTypeCountFromResourceTypesSummarizedArray + } + else { + $resourceCount = '0' + } + $recommendation = "Create diagnostics policy for this ResourceType. To verify GA check docs " + $null = $diagnosticsPolicyAnalysis.Add([PSCustomObject]@{ + Priority = '2-Medium' + PolicyId = 'n/a' + PolicyCategory = 'n/a' + PolicyName = 'n/a' + PolicyDeploysRoles = 'n/a' + ResourceType = $resourceTypeDiagnosticsCapable.ResourceType + ResourceTypeCount = $resourceCount + Status = 'n/a' + LogsSupported = $logsSupported + LogCategoriesInPolicy = 'n/a' + LogCategoriesSupported = $supportedLogs -join "$CsvDelimiterOpposite " + LogCategoriesDelta = 'n/a' + Recommendation = $recommendation + DiagnosticsTargetType = 'n/a' + PolicyForResourceTypeExists = $false + PolicyAssignments = 'n/a' + PolicyUsedInPolicySet = 'n/a' + PolicySetAssignments = 'n/a' + }) + } + } + } + + $diagnosticsPolicyAnalysisCount = ($diagnosticsPolicyAnalysis).count + + if ($diagnosticsPolicyAnalysisCount -gt 0) { + $tfCount = $diagnosticsPolicyAnalysisCount + + $htmlTableId = 'TenantSummary_DiagnosticsLifecycle' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Create Custom Policies for Azure ResourceTypes that support Diagnostics Logs and Metrics Create-AzDiagPolicy
+ Supported categories for Azure Resource Logs docs + + + + + + + + + + + + + + + + + + + +"@) + + foreach ($diagnosticsFinding in $diagnosticsPolicyAnalysis | Sort-Object -Property @{Expression = { $_.Priority } }, @{Expression = { $_.Recommendation } }, @{Expression = { $_.ResourceType } }, @{Expression = { $_.PolicyName } }, @{Expression = { $_.PolicyId } }) { + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + + + +"@) + } + [void]$htmlTenantSummary.AppendLine(@" + +
PriorityRecommendationResourceTypeResource CountDiagnostics capable (logs)Policy IdPolicy DisplayNameRole definitionsTargetLog Categories not covered by PolicyPolicy assignmentsPolicy used in PolicySetPolicySet assignments
+ $($diagnosticsFinding.Priority) + + $($diagnosticsFinding.Recommendation) + + $($diagnosticsFinding.ResourceType) + + $($diagnosticsFinding.ResourceTypeCount) + + $($diagnosticsFinding.LogsSupported) + + $($diagnosticsFinding.PolicyId) + + $($diagnosticsFinding.PolicyName) + + $($diagnosticsFinding.PolicyDeploysRoles) + + $($diagnosticsFinding.DiagnosticsTargetType) + + $($diagnosticsFinding.LogCategoriesDelta) + + $($diagnosticsFinding.PolicyAssignments) + + $($diagnosticsFinding.PolicyUsedInPolicySet) + + $($diagnosticsFinding.PolicySetAssignments) +
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No ResourceDiagnostics Policy Lifecycle recommendations

+'@) + } + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No ResourceDiagnostics Policy Lifecycle recommendations

+'@) + } + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No ResourceDiagnostics Policy Lifecycle recommendations

+'@) + } + $endsumDiagLifecycle = Get-Date + Write-Host " Resource Diagnostics Policy Lifecycle processing duration: $((New-TimeSpan -Start $startsumDiagLifecycle -End $endsumDiagLifecycle).TotalSeconds) seconds" + } + #endregion SUMMARYDiagnosticsPolicyLifecycle + + #endregion resources + } + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + + #endregion tenantSummaryDiagnostics + + showMemoryUsage + + #region tenantSummaryLimits + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + #region tenantSummaryLimitsTenant + [void]$htmlTenantSummary.AppendLine( @' +

Tenant

+'@) + + #policySets + if ($tenantCustompolicySetsCount -gt (($LimitPOLICYPolicySetDefinitionsScopedTenant * $LimitCriticalPercentage) / 100)) { + [void]$htmlTenantSummary.AppendLine(@" +

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

PolicySet definitions: $tenantCustompolicySetsCount/$LimitPOLICYPolicySetDefinitionsScopedTenant docs

+"@) + } + + #CustomRoleDefinitions + if ($tenantCustomRolesCount -gt (($LimitRBACCustomRoleDefinitionsTenant * $LimitCriticalPercentage) / 100)) { + [void]$htmlTenantSummary.AppendLine(@" +

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Custom Role definitions: $tenantCustomRolesCount/$LimitRBACCustomRoleDefinitionsTenant docs

+"@) + } + + #endregion tenantSummaryLimitsTenant + + #region tenantSummaryLimitsManagementGroups + [void]$htmlTenantSummary.AppendLine( @' +

Management Groups

+'@) + + #region SUMMARYMgsapproachingLimitsPolicyAssignments + Write-Host ' processing TenantSummary ManagementGroups Limit PolicyAssignments' + $mgsApproachingLimitPolicyAssignments = (($policyBaseQueryManagementGroups.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicyAndPolicySetAssignmentAtScopeCount -gt 0 -and (($_.PolicyAndPolicySetAssignmentAtScopeCount -gt ($LimitPOLICYPolicyAssignmentsManagementGroup * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, MgName, PolicyAssignmentAtScopeCount, PolicySetAssignmentAtScopeCount, PolicyAndPolicySetAssignmentAtScopeCount, PolicyAssignmentLimit -Unique) + if (($mgsApproachingLimitPolicyAssignments | Measure-Object).count -gt 0) { + $tfCount = ($mgsApproachingLimitPolicyAssignments | Measure-Object).count + $htmlTableId = 'TenantSummary_MgsapproachingLimitsPolicyAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYMgsapproachingLimitsPolicyAssignments = $null + $htmlSUMMARYMgsapproachingLimitsPolicyAssignments = foreach ($mgApproachingLimitPolicyAssignments in $mgsApproachingLimitPolicyAssignments) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYMgsapproachingLimitsPolicyAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdLimit
$($mgApproachingLimitPolicyAssignments.MgName -replace '<', '<' -replace '>', '>')$($mgApproachingLimitPolicyAssignments.MgId)$(($mgApproachingLimitPolicyAssignments.PolicyAndPolicySetAssignmentAtScopeCount/$LimitPOLICYPolicyAssignmentsManagementGroup).tostring('P')) ($($mgApproachingLimitPolicyAssignments.PolicyAndPolicySetAssignmentAtScopeCount)/$($LimitPOLICYPolicyAssignmentsManagementGroup)) ($($mgApproachingLimitPolicyAssignments.PolicyAssignmentAtScopeCount) Policy assignments, $($mgApproachingLimitPolicyAssignments.PolicySetAssignmentAtScopeCount) PolicySet assignments)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($mgsApproachingLimitPolicyAssignments | Measure-Object).count) Management Groups approaching Limit ($LimitPOLICYPolicyAssignmentsManagementGroup) for PolicyAssignment docs

+"@) + } + #endregion SUMMARYMgsapproachingLimitsPolicyAssignments + + #region SUMMARYMgsapproachingLimitsPolicyScope + Write-Host ' processing TenantSummary ManagementGroups Limit PolicyScope' + $mgsApproachingLimitPolicyScope = (($policyBaseQueryManagementGroups.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicyDefinitionsScopedCount -gt 0 -and (($_.PolicyDefinitionsScopedCount -gt ($LimitPOLICYPolicyDefinitionsScopedManagementGroup * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, MgName, PolicyDefinitionsScopedCount, PolicyDefinitionsScopedLimit -Unique) + if (($mgsApproachingLimitPolicyScope | Measure-Object).count -gt 0) { + $tfCount = ($mgsApproachingLimitPolicyScope | Measure-Object).count + $htmlTableId = 'TenantSummary_MgsapproachingLimitsPolicyScope' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYMgsapproachingLimitsPolicyScope = $null + $htmlSUMMARYMgsapproachingLimitsPolicyScope = foreach ($mgApproachingLimitPolicyScope in $mgsApproachingLimitPolicyScope) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYMgsapproachingLimitsPolicyScope) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdLimit
$($mgApproachingLimitPolicyScope.MgName -replace '<', '<' -replace '>', '>')$($mgApproachingLimitPolicyScope.MgId)$(($mgApproachingLimitPolicyScope.PolicyDefinitionsScopedCount/$LimitPOLICYPolicyDefinitionsScopedManagementGroup).tostring('P')) $($mgApproachingLimitPolicyScope.PolicyDefinitionsScopedCount)/$($LimitPOLICYPolicyDefinitionsScopedManagementGroup)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($mgsApproachingLimitPolicyScope.count) Management Groups approaching Limit ($LimitPOLICYPolicyDefinitionsScopedManagementGroup) for Policy Scope docs

+"@) + } + #endregion SUMMARYMgsapproachingLimitsPolicyScope + + #region SUMMARYMgsapproachingLimitsPolicySetScope + Write-Host ' processing TenantSummary ManagementGroups Limit PolicySetScope' + $mgsApproachingLimitPolicySetScope = (($policyBaseQueryManagementGroups.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicySetDefinitionsScopedCount -gt 0 -and (($_.PolicySetDefinitionsScopedCount -gt ($LimitPOLICYPolicySetDefinitionsScopedManagementGroup * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, MgName, PolicySetDefinitionsScopedCount, PolicySetDefinitionsScopedLimit -Unique) + if ($mgsApproachingLimitPolicySetScope.count -gt 0) { + $tfCount = ($mgsApproachingLimitPolicySetScope | Measure-Object).count + $htmlTableId = 'TenantSummary_MgsapproachingLimitsPolicySetScope' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYMgsapproachingLimitsPolicySetScope = $null + $htmlSUMMARYMgsapproachingLimitsPolicySetScope = foreach ($mgApproachingLimitPolicySetScope in $mgsApproachingLimitPolicySetScope) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYMgsapproachingLimitsPolicySetScope) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdLimit
$($mgApproachingLimitPolicySetScope.MgName -replace '<', '<' -replace '>', '>')$($mgApproachingLimitPolicySetScope.MgId)$(($mgApproachingLimitPolicySetScope.PolicySetDefinitionsScopedCount/$LimitPOLICYPolicySetDefinitionsScopedManagementGroup).tostring('P')) ($($mgApproachingLimitPolicySetScope.PolicySetDefinitionsScopedCount)/$($LimitPOLICYPolicySetDefinitionsScopedManagementGroup))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($mgsApproachingLimitPolicySetScope | Measure-Object).count) Management Groups approaching Limit ($LimitPOLICYPolicySetDefinitionsScopedManagementGroup) for PolicySet Scope docs

+"@) + } + #endregion SUMMARYMgsapproachingLimitsPolicySetScope + + #region SUMMARYMgsapproachingLimitsRoleAssignment + Write-Host ' processing TenantSummary ManagementGroups Limit RoleAssignments' + $mgsApproachingRoleAssignmentLimit = $rbacBaseQuery.where( { [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.RoleAssignmentsCount -gt ($LimitRBACRoleAssignmentsManagementGroup * $LimitCriticalPercentage / 100) }) | Sort-Object -Property MgId -Unique | Select-Object -Property MgId, MgName, RoleAssignmentsCount, RoleAssignmentsLimit + + if (($mgsApproachingRoleAssignmentLimit).count -gt 0) { + $tfCount = ($mgsApproachingRoleAssignmentLimit).count + $htmlTableId = 'TenantSummary_MgsapproachingLimitsRoleAssignment' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure RBAC Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYMgsapproachingLimitsRoleAssignment = $null + $htmlSUMMARYMgsapproachingLimitsRoleAssignment = foreach ($mgApproachingRoleAssignmentLimit in $mgsApproachingRoleAssignmentLimit) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYMgsapproachingLimitsRoleAssignment) + [void]$htmlTenantSummary.AppendLine(@" + +
Management Group NameManagement Group IdLimit
$($mgApproachingRoleAssignmentLimit.MgName -replace '<', '<' -replace '>', '>')$($mgApproachingRoleAssignmentLimit.MgId)$(($mgApproachingRoleAssignmentLimit.RoleAssignmentsCount/$LimitRBACRoleAssignmentsManagementGroup).tostring('P')) ($($mgApproachingRoleAssignmentLimit.RoleAssignmentsCount)/$($LimitRBACRoleAssignmentsManagementGroup))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($mgApproachingRoleAssignmentLimit | Measure-Object).count) Management Groups approaching Limit ($LimitRBACRoleAssignmentsManagementGroup) for RoleAssignment docs

+"@) + } + #endregion SUMMARYMgsapproachingLimitsRoleAssignment + + #endregion tenantSummaryLimitsManagementGroups + + #region tenantSummaryLimitsSubscriptions + [void]$htmlTenantSummary.AppendLine( @' +

Subscriptions

+'@) + + #region SUMMARYSubsapproachingLimitsResourceGroups + Write-Host ' processing TenantSummary Subscriptions Limit Resource Groups' + $subscriptionsApproachingLimitFromResourceGroupsAll = $resourceGroupsAll.where( { $_.count_ -gt ($LimitResourceGroups * ($LimitCriticalPercentage / 100)) }) + if (($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count -gt 0) { + $tfCount = ($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsResourceGroups' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Subscription Resource Group Limit docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsResourceGroups = $null + $htmlSUMMARYSubsapproachingLimitsResourceGroups = foreach ($subscriptionApproachingLimitFromResourceGroupsAll in $subscriptionsApproachingLimitFromResourceGroupsAll) { + $subscriptionData = $htSubDetails.($subscriptionApproachingLimitFromResourceGroupsAll.subscriptionId).details + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsResourceGroups) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionData.subscription -replace '<', '<' -replace '>', '>')$($subscriptionData.subscriptionId)$(($subscriptionApproachingLimitFromResourceGroupsAll.count_/$LimitResourceGroups).tostring('P')) ($($subscriptionApproachingLimitFromResourceGroupsAll.count_)/$($LimitResourceGroups))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" + $(($subscriptionsApproachingLimitFromResourceGroupsAll | Measure-Object).count) Subscriptions approaching Limit ($LimitResourceGroups) for ResourceGroups docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsResourceGroups + + #region SUMMARYSubsapproachingLimitsSubscriptionTags + Write-Host ' processing TenantSummary Subscriptions Limit Subscription Tags' + $subscriptionsApproachingLimitTags = ($optimizedTableForPathQueryMgAndSub.where( { (($_.SubscriptionTagsCount -gt ($LimitTagsSubscription * ($LimitCriticalPercentage / 100)))) })) + if (($subscriptionsApproachingLimitTags | Measure-Object).count -gt 0) { + $tfCount = ($subscriptionsApproachingLimitTags | Measure-Object).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsSubscriptionTags' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Subscription Tag Limit docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsSubscriptionTags = $null + $htmlSUMMARYSubsapproachingLimitsSubscriptionTags = foreach ($subscriptionApproachingLimitTags in $subscriptionsApproachingLimitTags) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsSubscriptionTags) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionApproachingLimitTags.subscription -replace '<', '<' -replace '>', '>')$($subscriptionApproachingLimitTags.subscriptionId)$(($subscriptionApproachingLimitTags.SubscriptionTagsCount/$LimitTagsSubscription).tostring('P')) ($($subscriptionApproachingLimitTags.SubscriptionTagsCount)/$($LimitTagsSubscription))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($subscriptionsApproachingLimitTags.count) Subscriptions approaching Limit ($LimitTagsSubscription) for Tags docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsSubscriptionTags + + #region SUMMARYSubsapproachingLimitsPolicyAssignments + Write-Host ' processing TenantSummary Subscriptions Limit PolicyAssignments' + $subscriptionsApproachingLimitPolicyAssignments = (($policyBaseQuerySubscriptions.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicyAndPolicySetAssignmentAtScopeCount -gt 0 -and (($_.PolicyAndPolicySetAssignmentAtScopeCount -gt ($_.PolicyAssignmentLimit * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, Subscription, SubscriptionId, PolicyAssignmentAtScopeCount, PolicySetAssignmentAtScopeCount, PolicyAndPolicySetAssignmentAtScopeCount, PolicyAssignmentLimit -Unique) + if ($subscriptionsApproachingLimitPolicyAssignments.count -gt 0) { + $tfCount = ($subscriptionsApproachingLimitPolicyAssignments | Measure-Object).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsPolicyAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsPolicyAssignments = $null + $htmlSUMMARYSubsapproachingLimitsPolicyAssignments = foreach ($subscriptionApproachingLimitPolicyAssignments in $subscriptionsApproachingLimitPolicyAssignments) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsPolicyAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionApproachingLimitPolicyAssignments.subscription -replace '<', '<' -replace '>', '>')$($subscriptionApproachingLimitPolicyAssignments.subscriptionId)$(($subscriptionApproachingLimitPolicyAssignments.PolicyAndPolicySetAssignmentAtScopeCount/$subscriptionApproachingLimitPolicyAssignments.PolicyAssignmentLimit).tostring('P')) ($($subscriptionApproachingLimitPolicyAssignments.PolicyAndPolicySetAssignmentAtScopeCount)/$($subscriptionApproachingLimitPolicyAssignments.PolicyAssignmentLimit)) ($($subscriptionApproachingLimitPolicyAssignments.PolicyAssignmentAtScopeCount) Policy assignments, $($subscriptionApproachingLimitPolicyAssignments.PolicySetAssignmentAtScopeCount) PolicySet assignments)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($subscriptionsApproachingLimitPolicyAssignments | Measure-Object).count) Subscriptions approaching Limit ($LimitPOLICYPolicyAssignmentsSubscription) for PolicyAssignment docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsPolicyAssignments + + #region SUMMARYSubsapproachingLimitsPolicyScope + Write-Host ' processing TenantSummary Subscriptions Limit PolicyScope' + $subscriptionsApproachingLimitPolicyScope = (($policyBaseQuerySubscriptions.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicyDefinitionsScopedCount -gt 0 -and (($_.PolicyDefinitionsScopedCount -gt ($_.PolicyDefinitionsScopedLimit * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, Subscription, SubscriptionId, PolicyDefinitionsScopedCount, PolicyDefinitionsScopedLimit -Unique) + if (($subscriptionsApproachingLimitPolicyScope | Measure-Object).count -gt 0) { + $tfCount = ($subscriptionsApproachingLimitPolicyScope | Measure-Object).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsPolicyScope' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsPolicyScope = $null + $htmlSUMMARYSubsapproachingLimitsPolicyScope = foreach ($subscriptionApproachingLimitPolicyScope in $subscriptionsApproachingLimitPolicyScope) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsPolicyScope) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionApproachingLimitPolicyScope.subscription -replace '<', '<' -replace '>', '>')$($subscriptionApproachingLimitPolicyScope.subscriptionId)$(($subscriptionApproachingLimitPolicyScope.PolicyDefinitionsScopedCount/$subscriptionApproachingLimitPolicyScope.PolicyDefinitionsScopedLimit).tostring('P')) ($($subscriptionApproachingLimitPolicyScope.PolicyDefinitionsScopedCount)/$($subscriptionApproachingLimitPolicyScope.PolicyDefinitionsScopedLimit))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$($subscriptionsApproachingLimitPolicyScope.count) Subscriptions approaching Limit ($LimitPOLICYPolicyDefinitionsScopedSubscription) for Policy Scope docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsPolicyScope + + #region SUMMARYSubsapproachingLimitsPolicySetScope + Write-Host ' processing TenantSummary Subscriptions Limit PolicySetScope' + $subscriptionsApproachingLimitPolicySetScope = (($policyBaseQuerySubscriptions.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.PolicySetDefinitionsScopedCount -gt 0 -and (($_.PolicySetDefinitionsScopedCount -gt ($_.PolicySetDefinitionsScopedLimit * ($LimitCriticalPercentage / 100)))) })) | Select-Object MgId, Subscription, SubscriptionId, PolicySetDefinitionsScopedCount, PolicySetDefinitionsScopedLimit -Unique) + if ($subscriptionsApproachingLimitPolicySetScope.count -gt 0) { + $tfCount = ($subscriptionsApproachingLimitPolicySetScope | Measure-Object).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsPolicySetScope' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure Policy Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsPolicySetScope = $null + $htmlSUMMARYSubsapproachingLimitsPolicySetScope = foreach ($subscriptionApproachingLimitPolicySetScope in $subscriptionsApproachingLimitPolicySetScope) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsPolicySetScope) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionApproachingLimitPolicySetScope.subscription -replace '<', '<' -replace '>', '>')$($subscriptionApproachingLimitPolicySetScope.subscriptionId)$(($subscriptionApproachingLimitPolicySetScope.PolicySetDefinitionsScopedCount/$subscriptionApproachingLimitPolicySetScope.PolicySetDefinitionsScopedLimit).tostring('P')) ($($subscriptionApproachingLimitPolicySetScope.PolicySetDefinitionsScopedCount)/$($subscriptionApproachingLimitPolicySetScope.PolicySetDefinitionsScopedLimit))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$(($subscriptionsApproachingLimitPolicyScope | Measure-Object).count) Subscriptions approaching Limit ($LimitPOLICYPolicySetDefinitionsScopedSubscription) for PolicySet Scope docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsPolicySetScope + + #region SUMMARYSubsapproachingLimitsRoleAssignment + Write-Host ' processing TenantSummary Subscriptions Limit RoleAssignments' + $subscriptionsApproachingRoleAssignmentLimit = $rbacBaseQuery.where( { -not [String]::IsNullOrEmpty($_.SubscriptionId) -and $_.RoleAssignmentsCount -gt ($_.RoleAssignmentsLimit * $LimitCriticalPercentage / 100) }) | Sort-Object -Property SubscriptionId -Unique | Select-Object -Property MgId, SubscriptionId, Subscription, RoleAssignmentsCount, RoleAssignmentsLimit + + $availableSubscriptionsRoleAssignmentLimits = ($htSubscriptionsRoleAssignmentLimit.values | Sort-Object -Unique) -join ' | ' + + if (($subscriptionsApproachingRoleAssignmentLimit).count -gt 0) { + $tfCount = ($subscriptionsApproachingRoleAssignmentLimit).count + $htmlTableId = 'TenantSummary_SubsapproachingLimitsRoleAssignment' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Azure RBAC Limits docs
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYSubsapproachingLimitsRoleAssignment = $null + $htmlSUMMARYSubsapproachingLimitsRoleAssignment = foreach ($subscriptionApproachingRoleAssignmentLimit in $subscriptionsApproachingRoleAssignmentLimit) { + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYSubsapproachingLimitsRoleAssignment) + [void]$htmlTenantSummary.AppendLine(@" + +
SubscriptionSubscriptionIdLimit
$($subscriptionApproachingRoleAssignmentLimit.subscription -replace '<', '<' -replace '>', '>')$($subscriptionApproachingRoleAssignmentLimit.subscriptionId)$(($subscriptionApproachingRoleAssignmentLimit.RoleAssignmentsCount/$subscriptionApproachingRoleAssignmentLimit.RoleAssignmentsLimit).tostring('P')) ($($subscriptionApproachingRoleAssignmentLimit.RoleAssignmentsCount)/$($subscriptionApproachingRoleAssignmentLimit.RoleAssignmentsLimit))
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" + $(($subscriptionsApproachingRoleAssignmentLimit | Measure-Object).count) Subscriptions approaching Limit ($availableSubscriptionsRoleAssignmentLimits) for RoleAssignment docs

+"@) + } + #endregion SUMMARYSubsapproachingLimitsRoleAssignment + + #endregion tenantSummaryLimitsSubscriptions + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryLimits + + showMemoryUsage + + #region tenantSummaryAAD + [void]$htmlTenantSummary.AppendLine(@' + +
+ Check out AzADServicePrincipalInsights GitHub
+ Demystifying Service Principals - Managed Identities devBlogs
+ John Savill - Azure AD App Registrations, Enterprise Apps and Service Principals YouTube
+'@) + + #region AADSPNotFound + Write-Host ' processing TenantSummary AAD ServicePrincipals - not found' + + if ($servicePrincipalRequestResourceNotFoundCount -gt 0) { + $tfCount = $servicePrincipalRequestResourceNotFoundCount + $htmlTableId = 'TenantSummary_AADSPNotFound' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ + + + + + + +"@) + $htmlSUMMARYAADSPNotFound = $null + $htmlSUMMARYAADSPNotFound = foreach ($serviceprincipal in $arrayServicePrincipalRequestResourceNotFound | Sort-Object) { + + @" + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPNotFound) + [void]$htmlTenantSummary.AppendLine(@" + +
Service Principal Object Id
$($serviceprincipal)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No ServicePrincipals where the API returned 'Request_ResourceNotFound'

+'@) + } + #endregion AADSPNotFound + + #region AADAppNotFound + Write-Host ' processing TenantSummary AAD Applications - not found' + + if ($applicationRequestResourceNotFoundCount -gt 0) { + $tfCount = $applicationRequestResourceNotFoundCount + $htmlTableId = 'TenantSummary_AADAppNotFound' + + [void]$htmlTenantSummary.AppendLine(@" + +
+ + + + + + + +"@) + $htmlSUMMARYAADAppNotFound = $null + $htmlSUMMARYAADAppNotFound = foreach ($app in $arrayApplicationRequestResourceNotFound | Sort-Object) { + + @" + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADAppNotFound) + [void]$htmlTenantSummary.AppendLine(@" + +
Application (Client) Id
$($app)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No Applications where the API returned 'Request_ResourceNotFound'

+'@) + } + #endregion AADAppNotFound + + #region AADSPManagedIdentity + $startAADSPManagedIdentityLoop = Get-Date + Write-Host ' processing TenantSummary AAD SP Managed Identities' + + if ($servicePrincipalsOfTypeManagedIdentityCount -gt 0) { + $tfCount = $servicePrincipalsOfTypeManagedIdentityCount + $htmlTableId = 'TenantSummary_AADSPManagedIdentities' + + if ($htOrphanedSPMI.keys.Count -gt 0) { + $orphanedSPMIPresent = $true + } + + $abbr = " " + $abbrOrphanedSPMI = " " + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYAADSPManagedIdentities = $null + $htmlSUMMARYAADSPManagedIdentities = foreach ($serviceprincipalMI in $servicePrincipalsOfTypeManagedIdentity | Sort-Object) { + + $serviceprincipalMIDetailed = $htServicePrincipals.($serviceprincipalMI) + $miRoleAssignments = 'n/a' + $miType = 'unknown' + $userMiAssignedToResourcesCount = '' + foreach ($altName in $serviceprincipalMIDetailed.alternativeNames) { + if ($altName -like 'isExplicit=*') { + $splitAltName = $altName.split('=') + if ($splitAltName[1] -eq 'true') { + $miType = 'User assigned' + if ($htUserAssignedIdentitiesAssignedResources.($serviceprincipalMI)) { + $userMiAssignedToResourcesCount = $htUserAssignedIdentitiesAssignedResources.($serviceprincipalMI).ResourcesCount + } + } + if ($splitAltName[1] -eq 'false') { + $miType = 'System assigned' + } + } + else { + $s1 = $altName -replace '.*/providers/'; $rm = $s1 -replace '.*/'; $resourceType = $s1 -replace "/$($rm)" + $miAlternativeName = $altname + $miResourceType = $resourceType + } + } + + if ($miResourceType -eq 'Microsoft.Authorization/policyAssignments') { + $policyAssignmentId = $miAlternativeName.ToLower() + if ($policyAssignmentId -like '/providers/Microsoft.Management/managementGroups/*') { + if (-not ($htCacheAssignmentsPolicy).($policyAssignmentId)) { + $assignmentInfo = 'n/a' + } + else { + $assignmentInfo = ($htCacheAssignmentsPolicy).($policyAssignmentId).Assignment + } + } + else { + #sub + if (((($policyAssignmentId).Split('/') | Measure-Object).Count - 1) -eq 6) { + if (-not ($htCacheAssignmentsPolicy).($policyAssignmentId)) { + $assignmentInfo = 'n/a' + } + else { + $assignmentInfo = ($htCacheAssignmentsPolicy).($policyAssignmentId).Assignment + } + } + else { + #rg + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not ($htCacheAssignmentsPolicyOnResourceGroupsAndResources).($policyAssignmentId)) { + $assignmentInfo = 'n/a' + } + else { + $assignmentInfo = ($htCacheAssignmentsPolicyOnResourceGroupsAndResources).($policyAssignmentId) + } + } + else { + if (-not ($htCacheAssignmentsPolicy).($policyAssignmentId)) { + $assignmentInfo = 'n/a' + } + else { + $assignmentInfo = ($htCacheAssignmentsPolicy).($policyAssignmentId).Assignment + } + } + } + } + + if ($assignmentinfo -ne 'n/a') { + + if ($assignmentinfo.id -like '/subscriptions/*/resourcegroups/*') { + + if ($assignmentInfo.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { + $policyAssignmentsPolicyVariant = 'Policy' + $policyAssignmentsPolicyVariant4ht = 'policy' + } + + if ($assignmentInfo.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { + $policyAssignmentsPolicyVariant = 'PolicySet' + $policyAssignmentsPolicyVariant4ht = 'policySet' + } + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).ToLower() + $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace '.*/' + + if ($policyAssignmentsPolicyVariant4ht -eq 'policy') { + if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + if ($policyAssignmentsPolicyVariant4ht -eq 'policySet') { + if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + + } + else { + $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).ToLower() + $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace '.*/' + + if ($policyAssignmentsPolicyVariant4ht -eq 'policy') { + if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + if ($policyAssignmentsPolicyVariant4ht -eq 'policySet') { + if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + } + } + else { + if ($assignmentInfo.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policyDefinitions/*') { + $policyAssignmentsPolicyVariant = 'Policy' + $policyAssignmentsPolicyVariant4ht = 'policy' + } + if ($assignmentInfo.properties.policyDefinitionId -like '*/providers/Microsoft.Authorization/policySetDefinitions/*') { + $policyAssignmentsPolicyVariant = 'PolicySet' + $policyAssignmentsPolicyVariant4ht = 'policySet' + } + + $policyAssignmentsPolicyDefinitionId = ($assignmentInfo.properties.policyDefinitionId).Tolower() + $policyAssignmentspolicyDefinitionIdGuid = $policyAssignmentsPolicyDefinitionId -replace '.*/' + + if ($policyAssignmentsPolicyVariant4ht -eq 'policy') { + if (($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicy).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + if ($policyAssignmentsPolicyVariant4ht -eq 'policySet') { + if (($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId)) { + $definitionInfo = ($htCacheDefinitionsPolicySet).($policyAssignmentsPolicyDefinitionId) + } + else { + $definitionInfo = 'unknown' + } + } + } + + if ($definitionInfo -eq 'unknown') { + $policyAssignmentMoreInfo = "unknown definition ($($policyAssignmentsPolicyDefinitionId))" + } + else { + if ($definitionInfo.type -eq 'BuiltIn') { + $policyAssignmentMoreInfo = "$($definitionInfo.Type) $($policyAssignmentsPolicyVariant): $($definitionInfo.LinkToAzAdvertizer) ($policyAssignmentspolicyDefinitionIdGuid)" + } + else { + $policyAssignmentMoreInfo = "$($definitionInfo.Type) $($policyAssignmentsPolicyVariant): $($definitionInfo.DisplayName -replace '<', '<' -replace '>', '>') ($($policyAssignmentsPolicyDefinitionId))" + } + } + } + else { + $policyAssignmentMoreInfo = 'n/a' + } + + } + else { + $policyAssignmentMoreInfo = 'n/a' + } + + if ($htRoleAssignmentsForServicePrincipals.($serviceprincipalMI)) { + + $arrayMiRoleAssignments = @() + $helperMiRoleAssignments = $htRoleAssignmentsForServicePrincipals.($serviceprincipalMI).RoleAssignments + + foreach ($roleAssignment in $helperMiRoleAssignments) { + if ($roleAssignment.RoleIsCustom -eq 'False') { + $arrayMiRoleAssignments += "$(($htCacheDefinitionsRole).($roleAssignment.roleDefinitionId).LinkToAzAdvertizer) ($($roleAssignment.roleassignmentId))" + } + else { + $arrayMiRoleAssignments += "$($roleAssignment.roleDefinitionName -replace '<', '<' -replace '>', '>'); $($roleAssignment.roleDefinitionId) ($($roleAssignment.roleassignmentId))" + } + } + $miRoleAssignments = "$(($arrayMiRoleAssignments).Count) ($($arrayMiRoleAssignments -join ', '))" + } + + $orphanedMI = '' + if ($miResourceType -eq 'Microsoft.Authorization/policyAssignments') { + $orphanedMI = 'false' + if ($htOrphanedSPMI.($serviceprincipalMI)) { + $orphanedMI = 'true' + } + } + + @" + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPManagedIdentities) + [void]$htmlTenantSummary.AppendLine(@" + +
ApplicationIdDisplayNameSP ObjectIdTypeUsageUsage infoPolicy assignment detailsRole assignmentsAssigned to resources$($abbr)Orphaned$($abbrOrphanedSPMI) +
$($serviceprincipalMIDetailed.appId)$($serviceprincipalMIDetailed.displayName)$($serviceprincipalMI)$miType$miResourceType$($serviceprincipalMIDetailed.alternativeNames -join ', ')$($policyAssignmentMoreInfo)$($miRoleAssignments)$userMiAssignedToResourcesCount$orphanedMI
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$servicePrincipalsOfTypeManagedIdentityCount AAD ServicePrincipals type=ManagedIdentity

+"@) + } + + $endAADSPManagedIdentityLoop = Get-Date + Write-Host " TenantSummary AAD SP Managed Identities processing duration: $((New-TimeSpan -Start $startAADSPManagedIdentityLoop -End $endAADSPManagedIdentityLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADSPManagedIdentityLoop -End $endAADSPManagedIdentityLoop).TotalSeconds) seconds)" + #endregion AADSPManagedIdentity + + #region AADSPCredExpiry + if (-not $skipApplications) { + $startAADSPCredExpiryLoop = Get-Date + Write-Host ' processing TenantSummary AAD SP Apps CredExpiry' + + $servicePrincipalsOfTypeApplicationCount = ($servicePrincipalsOfTypeApplication).Count + + if ($servicePrincipalsOfTypeApplicationCount -gt 0) { + $tfCount = $servicePrincipalsOfTypeApplicationCount + $htmlTableId = 'TenantSummary_AADSPCredExpiry' + + $servicePrincipalsOfTypeApplicationSecretsExpiring = $servicePrincipalsOfTypeApplication.where( { $htAppDetails.($_).appPasswordCredentialsGracePeriodExpiryCount -gt 0 } ) + $servicePrincipalsOfTypeApplicationSecretsExpiringCount = ($servicePrincipalsOfTypeApplicationSecretsExpiring).Count + $servicePrincipalsOfTypeApplicationCertificatesExpiring = $servicePrincipalsOfTypeApplication.where( { $htAppDetails.($_).appKeyCredentialsGracePeriodExpiryCount -gt 0 } ) + $servicePrincipalsOfTypeApplicationCertificatesExpiringCount = ($servicePrincipalsOfTypeApplicationCertificatesExpiring).Count + if ($servicePrincipalsOfTypeApplicationSecretsExpiringCount -gt 0 -or $servicePrincipalsOfTypeApplicationCertificatesExpiringCount -gt 0) { + $warningOrNot = "" + } + else { + $warningOrNot = "" + } + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYAADSPCredExpiry = $null + $htmlSUMMARYAADSPCredExpiry = foreach ($serviceprincipalApp in $servicePrincipalsOfTypeApplication | Sort-Object) { + @" + + + + + + +"@ + if ($htAppDetails.$serviceprincipalApp.appPasswordCredentialsCount) { + @" + + + + + +"@ + } + else { + @' + + + + + +'@ + } + + if ($htAppDetails.$serviceprincipalApp.appKeyCredentialsCount) { + @" + + + + + +"@ + } + else { + @' + + + + + +'@ + } + + @' + +'@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPCredExpiry) + [void]$htmlTenantSummary.AppendLine(@" + +
ApplicationIdDisplayNameNotesSP ObjectIdApp ObjectIdSecretsSecrets expiredSecrets expiry
<$($AADServicePrincipalExpiryWarningDays)d
Secrets expiry
>$($AADServicePrincipalExpiryWarningDays)d & <2y
Secrets expiry
>2y
CertsCerts expiredCerts expiry
<$($AADServicePrincipalExpiryWarningDays)d
Certs expiry
>$($AADServicePrincipalExpiryWarningDays)d & <2y
Certs expiry
>2y
$($htAppDetails.$serviceprincipalApp.spGraphDetails.appId)$($htAppDetails.$serviceprincipalApp.spGraphDetails.displayName)$($htAppDetails.$serviceprincipalApp.spGraphDetails.notes)$($htAppDetails.$serviceprincipalApp.spGraphDetails.Id)$($htAppDetails.$serviceprincipalApp.appGraphDetails.Id)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsCount)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiredCount)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsGracePeriodExpiryCount)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiryOKCount)$($htAppDetails.$serviceprincipalApp.appPasswordCredentialsExpiryOKMoreThan2YearsCount)00000$($htAppDetails.$serviceprincipalApp.appKeyCredentialsCount)$($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiredCount)$($htAppDetails.$serviceprincipalApp.appKeyCredentialsGracePeriodExpiryCount)$($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiryOKCount)$($htAppDetails.$serviceprincipalApp.appKeyCredentialsExpiryOKMoreThan2YearsCount)00000
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$servicePrincipalsOfTypeApplicationCount AAD ServicePrincipals type=Application

+"@) + } + + $endAADSPCredExpiryLoop = Get-Date + Write-Host " TenantSummary AAD SP Apps CredExpiry processing duration: $((New-TimeSpan -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADSPCredExpiryLoop -End $endAADSPCredExpiryLoop).TotalSeconds) seconds)" + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No information on AAD ServicePrincipals type=Application as Guest account does not have enough permissions

+'@) + } + #endregion AADSPCredExpiry + + #region AADSPExternalSP + Write-Host ' processing TenantSummary AAD External ServicePrincipals' + $startAADSPExternalSP = Get-Date + + $htRoleAssignmentsForServicePrincipalsRgRes = @{} + $roleAssignmentsForServicePrincipalsRgRes = (((($htCacheAssignmentsRBACOnResourceGroupsAndResources).values).where( { $_.ObjectType -eq 'ServicePrincipal' })) | Sort-Object -Property RoleAssignmentId -Unique) + foreach ($spWithRoleAssignment in $roleAssignmentsForServicePrincipalsRgRes | Group-Object -Property ObjectId) { + if (-not $htRoleAssignmentsForServicePrincipalsRgRes.($spWithRoleAssignment.Name)) { + $htRoleAssignmentsForServicePrincipalsRgRes.($spWithRoleAssignment.Name) = @{} + $htRoleAssignmentsForServicePrincipalsRgRes.($spWithRoleAssignment.Name).RoleAssignments = $spWithRoleAssignment.group + } + } + + $appsWithOtherOrgId = $htServicePrincipals.Keys.where( { $htServicePrincipals.($_).servicePrincipalType -eq 'Application' -and $htServicePrincipals.($_).appOwnerOrganizationId -ne $azAPICallConf['checkContext'].Tenant.Id } ) + $appsWithOtherOrgIdCount = ($appsWithOtherOrgId).Count + + if ($appsWithOtherOrgIdCount -gt 0) { + $tfCount = $appsWithOtherOrgIdCount + $htmlTableId = 'TenantSummary_AADSPExternal' + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + $abbr = " " + } + else { + $abbr = '' + } + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYAADSPExternal = $null + $htmlSUMMARYAADSPExternal = foreach ($serviceprincipalApp in $appsWithOtherOrgId | Sort-Object) { + $arrayRoleAssignments4ExternalApp = [System.Collections.ArrayList]@() + $roleAssignmentsMgSub = $htRoleAssignmentsForServicePrincipals.($serviceprincipalApp).RoleAssignments + $roleAssignmentsMgSubCount = ($roleAssignmentsMgSub).Count + $roleAssignments4ExternalApp = 'n/a' + if ($roleAssignmentsMgSubCount -gt 0) { + $roleAssignments4ExternalApp = $roleAssignmentsMgSubCount + } + $roleAssignmentsRgRes = $htRoleAssignmentsForServicePrincipalsRgRes.($serviceprincipalApp).RoleAssignments + $roleAssignmentsRgResCount = ($roleAssignmentsRgRes).Count + if ($roleAssignmentsRgResCount -gt 0) { + foreach ($roleAssignmentRgRes in $roleAssignmentsRgRes) { + $null = $arrayRoleAssignments4ExternalApp.Add([PSCustomObject]@{ + roleAssignmentId = $roleAssignmentRgRes.RoleAssignmentId + }) + } + $roleAssignments4ExternalApp = "$roleAssignmentsRgResCount ($($arrayRoleAssignments4ExternalApp.roleAssignmentId -join ', '))" + } + + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYAADSPExternal) + [void]$htmlTenantSummary.AppendLine(@" + +
ApplicationIdDisplayNameSP ObjectIdOrganizationIdRole assignments$($abbr)
$($htServicePrincipals.($serviceprincipalApp).appId)$($htServicePrincipals.($serviceprincipalApp).displayName)$($htServicePrincipals.($serviceprincipalApp).id)$($htServicePrincipals.($serviceprincipalApp).appOwnerOrganizationId)$roleAssignments4ExternalApp
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$appsWithOtherOrgIdCount External (appOwnerOrganizationId) AAD ServicePrincipals type=Application

+"@) + } + + $endAADSPExternalSP = Get-Date + Write-Host " TenantSummary AAD External ServicePrincipals processing duration: $((New-TimeSpan -Start $startAADSPExternalSP -End $endAADSPExternalSP).TotalMinutes) minutes ($((New-TimeSpan -Start $startAADSPExternalSP -End $endAADSPExternalSP).TotalSeconds) seconds)" + #endregion AADSPExternalSP + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryAAD + + showMemoryUsage + + #region tenantSummaryConsumption + [void]$htmlTenantSummary.AppendLine(@' + +
+ Customize your Azure environment optimizations (Cost, Reliability & more) with Azure Optimization Engine (AOE) +'@) + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + $startConsumption = Get-Date + Write-Host ' processing TenantSummary Consumption' + + if (($arrayConsumptionData | Measure-Object).Count -gt 0) { + $tfCount = ($arrayConsumptionData | Measure-Object).Count + $htmlTableId = 'TenantSummary_Consumption' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + +"@) + $htmlSUMMARYConsumption = $null + $htmlSUMMARYConsumption = foreach ($consumptionLine in $arrayConsumptionData) { + @" + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYConsumption) + [void]$htmlTenantSummary.AppendLine(@" + +
ChargeTypeResourceTypeCategoryResourceCountCost ($($AzureConsumptionPeriod)d)CurrencySubscriptions
$($consumptionLine.ConsumedServiceChargeType)$($consumptionLine.ResourceType)$($consumptionLine.ConsumedServiceCategory)$($consumptionLine.ConsumedServiceInstanceCount)$($consumptionLine.ConsumedServiceCost)$($consumptionLine.ConsumedServiceCurrency)$($consumptionLine.ConsumedServiceSubscriptions)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No information on Consumption

+'@) + } + + $endConsumption = Get-Date + Write-Host " TenantSummary Consumption processing duration: $((New-TimeSpan -Start $startConsumption -End $endConsumption).TotalMinutes) minutes ($((New-TimeSpan -Start $startConsumption -End $endConsumption).TotalSeconds) seconds)" + + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

No information on Consumption as switch parameter -DoAzureConsumption was not applied

+'@) + } + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryConsumption + + showMemoryUsage + + #region tenantSummaryChangeTracking + Write-Host ' processing TenantSummary ChangeTracking' + $startChangeTracking = Get-Date + $xdaysAgo = (Get-Date).AddDays(-$ChangeTrackingDays) + + #region ctpolicydata + Write-Host ' processing Policy' + $customPolicyCreatedOrUpdated = ($customPoliciesDetailed.where( { (-not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo) -or (-not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo) })) + $customPolicyCreatedOrUpdatedCount = $customPolicyCreatedOrUpdated.Count + $customPolicyCreatedMgSub = ($customPolicyCreatedOrUpdated.where( { -not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo })) + $customPolicyCreatedMg = ($customPolicyCreatedMgSub.where( { $_.Scope -eq 'Mg' })) + $customPolicyCreatedMgCount = ($customPolicyCreatedMg).count + $customPolicyCreatedSub = ($customPolicyCreatedMgSub.where( { $_.Scope -eq 'Sub' })) + $customPolicyCreatedSubCount = ($customPolicyCreatedSub).count + + $customPolicyUpdatedMgSub = ($customPolicyCreatedOrUpdated.where( { -not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo })) + $customPolicyUpdatedMg = ($customPolicyUpdatedMgSub.where( { $_.Scope -eq 'Mg' })) + $customPolicyUpdatedMgCount = ($customPolicyUpdatedMg).count + $customPolicyUpdatedSub = ($customPolicyUpdatedMgSub.where( { $_.Scope -eq 'Sub' })) + $customPolicyUpdatedSubCount = ($customPolicyUpdatedSub).count + #endregion ctpolicydata + + #region ctpolicySetdata + Write-Host ' processing PolicySet' + $customPolicySetCreatedOrUpdated = ($customPolicySetsDetailed.where( { (-not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo) -or (-not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo) })) + $customPolicySetCreatedOrUpdatedCount = $customPolicySetCreatedOrUpdated.Count + + $customPolicySetCreatedMgSub = ($customPolicySetCreatedOrUpdated.where( { -not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo })) + $customPolicySetCreatedMg = ($customPolicySetCreatedMgSub.where( { $_.Scope -eq 'Mg' })) + $customPolicySetCreatedMgCount = ($customPolicySetCreatedMg).count + $customPolicySetCreatedSub = ($customPolicySetCreatedMgSub.where( { $_.Scope -eq 'Sub' })) + $customPolicySetCreatedSubCount = ($customPolicySetCreatedSub).count + + $customPolicySetUpdatedMgSub = ($customPolicySetCreatedOrUpdated.where( { -not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo })) + $customPolicySetUpdatedMg = ($customPolicySetUpdatedMgSub.where( { $_.Scope -eq 'Mg' })) + $customPolicySetUpdatedMgCount = ($customPolicySetUpdatedMg).count + $customPolicySetUpdatedSub = ($customPolicySetUpdatedMgSub.where( { $_.Scope -eq 'Sub' })) + $customPolicySetUpdatedSubCount = ($customPolicySetUpdatedSub).count + #endregion ctpolicySetdata + + #region ctpolicyAssignmentsData + Write-Host ' processing Policy assignment' + $policyAssignmentsCreatedOrUpdated = (($arrayPolicyAssignmentsEnriched.where( { $_.Inheritance -notlike 'inherited*' } )).where( { (-not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo) -or (-not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo) })) + $policyAssignmentsCreatedOrUpdatedCount = $policyAssignmentsCreatedOrUpdated.Count + + $policyAssignmentsCreatedMgSub = $policyAssignmentsCreatedOrUpdated.where( { (-not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo) }) + $policyAssignmentsCreatedMg = ($policyAssignmentsCreatedMgSub.where( { $_.mgOrSubOrRG -eq 'Mg' })) + $policyAssignmentsCreatedMgCount = ($policyAssignmentsCreatedMg).count + $policyAssignmentsCreatedSub = ($policyAssignmentsCreatedMgSub.where( { $_.mgOrSubOrRG -eq 'Sub' })) + $policyAssignmentsCreatedSubCount = ($policyAssignmentsCreatedSub).count + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + $policyAssignmentsCreatedRg = ($policyAssignmentsUpdatedMgSub.where( { $_.mgOrSubOrRG -eq 'RG' })) + $policyAssignmentsCreatedRgCount = ($policyAssignmentsCreatedRg).count + } + + $policyAssignmentsUpdatedMgSub = $policyAssignmentsCreatedOrUpdated.where( { (-not [string]::IsNullOrEmpty($_.UpdatedOn) -and [datetime]$_.UpdatedOn -gt $xdaysAgo) }) + $policyAssignmentsUpdatedMg = ($policyAssignmentsUpdatedMgSub.where( { $_.mgOrSubOrRG -eq 'Mg' })) + $policyAssignmentsUpdatedMgCount = ($policyAssignmentsUpdatedMg).count + $policyAssignmentsUpdatedSub = ($policyAssignmentsUpdatedMgSub.where( { $_.mgOrSubOrRG -eq 'Sub' })) + $policyAssignmentsUpdatedSubCount = ($policyAssignmentsUpdatedSub).count + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + $policyAssignmentsUpdatedRg = ($policyAssignmentsUpdatedMgSub.where( { $_.mgOrSubOrRG -eq 'RG' })) + $policyAssignmentsUpdatedRgCount = ($policyAssignmentsUpdatedRg).count + } + + + if ($customPolicyCreatedOrUpdatedCount -gt 0 -or $customPolicySetCreatedOrUpdatedCount -gt 0 -or $policyAssignmentsCreatedOrUpdatedCount -gt 0) { + $ctContenIndicatorPolicy = 'ctContenPolicyTrue' + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + $policyAssignmentSummaryCt = "(Mg: C:$($policyAssignmentsCreatedMgCount), U:$($policyAssignmentsUpdatedMgCount); Sub: C:$($policyAssignmentsCreatedSubCount), U:$($policyAssignmentsUpdatedSubCount)); RG: C:$($policyAssignmentsCreatedRgCount), U:$($policyAssignmentsUpdatedRgCount)" + } + else { + $policyAssignmentSummaryCt = "(Mg: C:$($policyAssignmentsCreatedMgCount), U:$($policyAssignmentsUpdatedMgCount); Sub: C:$($policyAssignmentsCreatedSubCount), U:$($policyAssignmentsUpdatedSubCount))" + } + + } + else { + $ctContenIndicatorPolicy = 'ctContenPolicyFalse' + $policyAssignmentSummaryCt = '' + } + #endregion ctpolicyAssignmentsData + + ##RBAC + #region ctRbacData + #rbac defs + Write-Host ' processing RBAC' + $customRoleDefinitionsCreatedOrUpdated = $tenantCustomRoles.where( { $_.IsCustom -eq $true -and $_.Json.properties.createdOn -gt $xdaysAgo -or $_.Json.properties.updatedOn -gt $xdaysAgo }) + $customRoleDefinitionsCreatedOrUpdatedCount = $customRoleDefinitionsCreatedOrUpdated.Count + + #rbac defs created + Write-Host ' processing RBAC Role definition created' + $customRoleDefinitionsCreated = $customRoleDefinitionsCreatedOrUpdated.where( { $_.Json.properties.createdOn -gt $xdaysAgo }) + $customRoleDefinitionsCreatedCount = $customRoleDefinitionsCreated.Count + + #rbac defs updated + Write-Host ' processing RBAC Role definition updated' + $customRoleDefinitionsUpdated = $customRoleDefinitionsCreatedOrUpdated.where( { $_.Json.properties.updatedOn -ne $_.Json.properties.createdOn -and $_.Json.properties.updatedOn -gt $xdaysAgo }) + $customRoleDefinitionsUpdatedCount = $customRoleDefinitionsUpdated.Count + #endregion ctRbacData + + #region ctrbacassignments + #rbac roleassignments + Write-Host ' processing RBAC Role assignments' + $roleAssignmentsCreated = ($rbacAll | Sort-Object -Property RoleAssignmentId, ObjectId -Unique).where( { -not [string]::IsNullOrEmpty($_.CreatedOn) -and [datetime]$_.CreatedOn -gt $xdaysAgo }) + $roleAssignmentsCreatedUnique = ($roleAssignmentsCreated | Sort-Object -Property RoleAssignmentId -Unique) + $roleAssignmentsCreatedCount = ($roleAssignmentsCreated | Sort-Object -Property RoleAssignmentId -Unique).Count + $roleAssignmentsCreatedImpactedIdentitiesCount = $roleAssignmentsCreated.Count + + #rbac assignments createdMg + $roleAssignmentsCreatedMg = $roleAssignmentsCreatedUnique.where( { $_.ScopeTenOrMgOrSubOrRGOrRes -eq 'MG' -or $_.ScopeTenOrMgOrSubOrRGOrRes -eq 'Ten' }) + $roleAssignmentsCreatedMgCount = $roleAssignmentsCreatedMg.Count + #rbac assignments createdSub + $roleAssignmentsCreatedSub = $roleAssignmentsCreatedUnique.where( { $_.ScopeTenOrMgOrSubOrRGOrRes -eq 'Sub' }) + $roleAssignmentsCreatedSubCount = $roleAssignmentsCreatedSub.Count + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + $roleAssignmentsCreatedSubRg = $roleAssignmentsCreatedUnique.where( { $_.ScopeTenOrMgOrSubOrRGOrRes -eq 'RG' }) + $roleAssignmentsCreatedSubRgCount = $roleAssignmentsCreatedSubRg.Count + $roleAssignmentsCreatedSubRgRes = $roleAssignmentsCreatedUnique.where( { $_.ScopeTenOrMgOrSubOrRGOrRes -eq 'Res' }) + $roleAssignmentsCreatedSubRgResCount = $roleAssignmentsCreatedSubRgRes.Count + } + + if ($customRoleDefinitionsCreatedOrUpdatedCount -gt 0 -or $roleAssignmentsCreatedCount -gt 0) { + $ctContenIndicatorRBAC = 'ctContenRBACTrue' + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + $rbacAssignmentSummaryCt = "(Mg: $roleAssignmentsCreatedMgCount; Sub: $roleAssignmentsCreatedSubCount; RG: $roleAssignmentsCreatedSubRgCount; Res: $roleAssignmentsCreatedSubRgResCount)" + } + else { + $rbacAssignmentSummaryCt = "(Mg: $roleAssignmentsCreatedMgCount; Sub: $roleAssignmentsCreatedSubCount)" + } + } + else { + $ctContenIndicatorRBAC = 'ctContenRBACFalse' + $rbacAssignmentSummaryCt = '' + } + #endregion ctrbacassignments + + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region ctresources + Write-Host ' processing Resources' + $resourcesCreatedOrChanged = $resourcesIdsAll.where( { $_.createdTime -gt $xdaysAgo -or $_.changedTime -gt $xdaysAgo }) + $resourcesCreatedOrChangedCount = $resourcesCreatedOrChanged.Count + + $resourcesCreatedAndChanged = $resourcesIdsAll.where( { $_.createdTime -gt $xdaysAgo -and $_.changedTime -gt $xdaysAgo }) + $resourcesCreatedAndChangedCount = $resourcesCreatedAndChanged.Count + + $resourcesCreated = $resourcesCreatedOrChanged.where( { $_.createdTime -gt $xdaysAgo }) + $resourcesCreatedCount = $resourcesCreated.Count + $resourcesChanged = $resourcesCreatedOrChanged.where( { $_.changedTime -gt $xdaysAgo }) + $resourcesChangedCount = $resourcesChanged.Count + + if ($resourcesCreatedOrChangedCount -gt 0) { + $ctContenIndicatorResources = 'ctContenResourcesTrue' + $resourcesCreatedOrChangedGrouped = $resourcesCreatedOrChanged | Group-Object -Property type + $resourcesCreatedOrChangedGroupedCount = ($resourcesCreatedOrChangedGrouped | Measure-Object).Count + } + else { + $ctContenIndicatorResources = 'ctContenResourcesFalse' + } + #endregion ctresources + } + + + + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + #region ctpolicy + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + #region ChangeTrackingCustomPolicy + if ($customPolicyCreatedOrUpdatedCount -gt 0) { + $tfCount = $customPolicyCreatedOrUpdatedCount + $htmlTableId = 'TenantSummary_ChangeTrackingCustomPolicy' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingCustomPolicy = $null + $htmlSUMMARYChangeTrackingCustomPolicy = foreach ($entry in $customPolicyCreatedOrUpdated | Sort-Object -Property CreatedOn, UpdatedOn -Descending) { + $createdOnGt = $false + if ($entry.CreatedOn -ne '') { + $createdOn = ($entry.CreatedOn) + if ([datetime]($entry.CreatedOn) -gt $xdaysAgo) { + $createdOnGt = $true + } + } + else { + $createdOn = '' + } + + $updatedOnGt = $false + if ($entry.updatedOn -ne '') { + $updatedOn = ($entry.UpdatedOn) + if ([datetime]($entry.UpdatedOn) -gt $xdaysAgo) { + $updatedOnGt = $true + } + $updatedOnGt = $true + } + else { + $updatedOn = '' + } + + $createOnUpdatedOn = $null + if ($createdOnGt) { + $createOnUpdatedOn = 'Created' + } + if ($updatedOnGt) { + $createOnUpdatedOn = 'Updated' + } + if ($createdOnGt -and $updatedOnGt) { + $createOnUpdatedOn = 'Created&Updated' + } + + if ($entry.UsedInPolicySetsCount -gt 0) { + $customPolicyUsedInPolicySets = "$($entry.UsedInPolicySetsCount) ($($entry.UsedInPolicySets))" + } + else { + $customPolicyUsedInPolicySets = $($entry.UsedInPolicySetsCount) + } + + @" + + + + + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingCustomPolicy) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScope IdPolicy DisplayNamePolicyIdCategoryEffectRole definitionsUnique assignmentsUsed in PolicySetsCreated/UpdatedCreatedOnCreatedByUpdatedOnUpdatedBy
$($entry.Scope)$($entry.ScopeId)$($entry.PolicyDisplayName -replace '<', '<' -replace '>', '>')$($entry.PolicyDefinitionId -replace '<', '<' -replace '>', '>')$($entry.PolicyCategory -replace '<', '<' -replace '>', '>')$($entry.PolicyEffect)$($entry.RoleDefinitions)$($entry.UniqueAssignments -replace '<', '<' -replace '>', '>')$($customPolicyUsedInPolicySets)$createOnUpdatedOn$($entry.CreatedOn)$($entry.CreatedBy)$($entry.UpdatedOn)$($entry.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$customPolicyCreatedOrUpdatedCount Created/Updated custom Policy definitions

+"@) + } + #endregion ChangeTrackingCustomPolicy + + #region ChangeTrackingCustomPolicySet + if ($customPolicySetCreatedOrUpdatedCount -gt 0) { + $tfCount = $customPolicySetCreatedOrUpdatedCount + $htmlTableId = 'TenantSummary_ChangeTrackingCustomPolicySet' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingCustomPolicySet = $null + $htmlSUMMARYChangeTrackingCustomPolicySet = foreach ($entry in $customPolicySetCreatedOrUpdated | Sort-Object -Property CreatedOn, UpdatedOn -Descending) { + $createdOnGt = $false + if ($entry.CreatedOn -ne '') { + $createdOn = ($entry.CreatedOn) + if ([datetime]($entry.CreatedOn) -gt $xdaysAgo) { + $createdOnGt = $true + } + } + else { + $createdOn = '' + } + + $updatedOnGt = $false + if ($entry.updatedOn -ne '') { + $updatedOn = ($entry.UpdatedOn) + if ([datetime]($entry.UpdatedOn) -gt $xdaysAgo) { + $updatedOnGt = $true + } + $updatedOnGt = $true + } + else { + $updatedOn = '' + } + + $createOnUpdatedOn = $null + if ($createdOnGt) { + $createOnUpdatedOn = 'Created' + } + if ($updatedOnGt) { + $createOnUpdatedOn = 'Updated' + } + if ($createdOnGt -and $updatedOnGt) { + $createOnUpdatedOn = 'Created&Updated' + } + + @" + + + + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingCustomPolicySet) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeScopeIdPolicySet DisplayNamePolicySetIdCategoryUnique assignmentsPolicies used in PolicySetCreated/UpdatedCreatedOnCreatedByUpdatedOnUpdatedBy
$($entry.Scope)$($entry.ScopeId)$($entry.PolicySetDisplayName -replace '<', '<' -replace '>', '>')$($entry.PolicySetDefinitionId -replace '<', '<' -replace '>', '>')$($entry.PolicySetCategory -replace '<', '<' -replace '>', '>')$($entry.UniqueAssignments -replace '<', '<' -replace '>', '>')$($entry.PoliciesUsed)$createOnUpdatedOn$($entry.CreatedOn)$($entry.CreatedBy)$($entry.UpdatedOn)$($entry.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$customPolicySetCreatedOrUpdatedCount Created/Updated custom PolicySet definitions

+"@) + } + #endregion ChangeTrackingCustomPolicySet + + #region ChangeTrackingPolicyAssignments + if ($policyAssignmentsCreatedOrUpdatedCount -gt 0) { + $tfCount = $policyAssignmentsCreatedOrUpdatedCount + $htmlTableId = 'TenantSummary_ChangeTrackingPolicyAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + +"@) + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + [void]$htmlTenantSummary.AppendLine(@' + + + + + +'@) + } + + [void]$htmlTenantSummary.AppendLine(@" + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingPolicyAssignments = $null + $htmlSUMMARYChangeTrackingPolicyAssignments = foreach ($policyAssignment in $policyAssignmentsCreatedOrUpdated | Sort-Object -Property CreatedOn, UpdatedOn -Descending) { + $createdOnGt = $false + if ($policyAssignment.CreatedOn -ne '') { + $createdOn = ($policyAssignment.CreatedOn) + if ([datetime]($policyAssignment.CreatedOn) -gt $xdaysAgo) { + $createdOnGt = $true + } + } + else { + $createdOn = '' + } + + $updatedOnGt = $false + if ($policyAssignment.updatedOn -ne '') { + $updatedOn = ($policyAssignment.UpdatedOn) + if ([datetime]($policyAssignment.UpdatedOn) -gt $xdaysAgo) { + $updatedOnGt = $true + } + $updatedOnGt = $true + } + else { + $updatedOn = '' + } + + $createOnUpdatedOn = $null + if ($createdOnGt) { + $createOnUpdatedOn = 'Created' + } + if ($updatedOnGt) { + $createOnUpdatedOn = 'Updated' + } + if ($createdOnGt -and $updatedOnGt) { + $createOnUpdatedOn = 'Created&Updated' + } + + if ($policyAssignment.PolicyType -eq 'Custom') { + $policyName = ($policyAssignment.PolicyName -replace '<', '<' -replace '>', '>') + } + else { + $policyName = $policyAssignment.PolicyName + } + + @" + + + + + + + + + + + + + + + + + + + +"@ + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + @" + + + + + +"@ + } + + @" + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingPolicyAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeManagement Group IdManagement Group NameSubscriptionIdSubscription NameInheritanceScopeExcludedExemption appliesPolicy/Set DisplayNamePolicy/Set DescriptionPolicy/SetIdPolicy/SetTypeCategoryEffectParametersEnforcementNonCompliance MessagePolicies NonCmplntPolicies CompliantResources NonCmplntResources CompliantResources ConflictingRole/Assignment $noteOrNotAssignment DisplayNameAssignment DescriptionAssignmentIdCreated/UpdatedAssignedByCreatedOnCreatedByUpdatedOnUpdatedBy
$($policyAssignment.mgOrSubOrRG)$($policyAssignment.MgId)$($policyAssignment.MgName -replace '<', '<' -replace '>', '>')$($policyAssignment.SubscriptionId)$($policyAssignment.SubscriptionName)$($policyAssignment.Inheritance)$($policyAssignment.ExcludedScope)$($policyAssignment.ExemptionScope)$($policyName)$($policyAssignment.PolicyDescription -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyId -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyVariant)$($policyAssignment.PolicyType)$($policyAssignment.PolicyCategory -replace '<', '<' -replace '>', '>')$($policyAssignment.Effect)$($policyAssignment.PolicyAssignmentParameters)$($policyAssignment.PolicyAssignmentEnforcementMode)$($policyAssignment.PolicyAssignmentNonComplianceMessages)$($policyAssignment.NonCompliantPolicies)$($policyAssignment.CompliantPolicies)$($policyAssignment.NonCompliantResources)$($policyAssignment.CompliantResources)$($policyAssignment.ConflictingResources)$($policyAssignment.RelatedRoleAssignments)$($policyAssignment.PolicyAssignmentDisplayName -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentDescription -replace '<', '<' -replace '>', '>')$($policyAssignment.PolicyAssignmentId -replace '<', '<' -replace '>', '>')$createOnUpdatedOn$($policyAssignment.AssignedBy)$($policyAssignment.CreatedOn)$($policyAssignment.CreatedBy)$($policyAssignment.UpdatedOn)$($policyAssignment.UpdatedBy)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$policyAssignmentsCreatedOrUpdatedCount Created/Updated Policy assignments

+"@) + } + #endregion ChangeTrackingPolicyAssignments + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + + #endregion ctpolicy + + #region ctrbac + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + #region ChangeTrackingCustomRoles + if ($customRoleDefinitionsCreatedOrUpdatedCount -gt 0) { + $tfCount = $customRoleDefinitionsCreatedOrUpdatedCount + $htmlTableId = 'TenantSummary_ChangeTrackingCustomRoles' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingCustomRoles = $null + $htmlSUMMARYChangeTrackingCustomRoles = foreach ($entry in $customRoleDefinitionsCreatedOrUpdated | Sort-Object @{Expression = { $_.Json.properties.createdOn } }, @{Expression = { $_.Json.properties.updatedOn } } -Descending) { + $createdBy = $entry.Json.properties.createdBy + if ($htIdentitiesWithRoleAssignmentsUnique.($createdBy)) { + $createdBy = $htIdentitiesWithRoleAssignmentsUnique.($createdBy).details + } + + $createdOn = $entry.Json.properties.createdOn + $createdOnFormated = $createdOn + $createdOnUpdatedOn = 'Created' + + $updatedOn = $entry.Json.properties.updatedOn + if ($updatedOn -eq $createdOn) { + $updatedOnFormated = '' + $updatedByRemoveNoiseOrNot = '' + } + else { + if ($createdOn -gt $xdaysAgo) { + $createdOnUpdatedOn = 'Created&Updated' + } + else { + $createdOnUpdatedOn = 'Updated' + } + $updatedOnFormated = $updatedOn + $updatedByRemoveNoiseOrNot = $entry.Json.properties.updatedBy + if ($htIdentitiesWithRoleAssignmentsUnique.($updatedByRemoveNoiseOrNot)) { + $updatedByRemoveNoiseOrNot = $htIdentitiesWithRoleAssignmentsUnique.($updatedByRemoveNoiseOrNot).details + } + } + + @" + + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingCustomRoles) + [void]$htmlTenantSummary.AppendLine(@" + +
Role NameRoleIdAssignable ScopesDataCreated/UpdatedCreatedOnCreatedByUpdatedOnUpdatedBy
$($entry.Name -replace '<', '<' -replace '>', '>')$($entry.Id)$(($entry.AssignableScopes | Measure-Object).count) ($($entry.AssignableScopes -join "$CsvDelimiterOpposite "))$($roleManageData)$createdOnUpdatedOn$createdOnFormated$createdBy$updatedOnFormated$updatedByRemoveNoiseOrNot
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$customRoleDefinitionsCreatedOrUpdatedCount Created/Updated custom Role definitions

+"@) + } + #endregion ChangeTrackingCustomRoles + + #region ChangeTrackingRoleAssignments + if ($roleAssignmentsCreatedCount -gt 0) { + $tfCount = $roleAssignmentsCreatedCount + $htmlTableId = 'TenantSummary_ChangeTrackingRoleAssignments' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingRoleAssignments = $null + $htmlSUMMARYChangeTrackingRoleAssignments = [System.Text.StringBuilder]::new() + foreach ($entry in $roleAssignmentsCreated | Sort-Object -Property CreatedOn -Descending) { + if ($entry.RoleType -eq 'Custom') { + $roleName = ($entry.Role -replace '<', '<' -replace '>', '>') + } + else { + $roleName = $entry.Role + } + [void]$htmlSUMMARYChangeTrackingRoleAssignments.AppendFormat( + @' + + + + + + + + + + + + + + + + + + + + + + +'@, $entry.ScopeTenOrMgOrSubOrRGOrRes, + $roleName, + $entry.RoleId, + $entry.RoleType, + $entry.RoleDataRelated, + $entry.ObjectDisplayName, + $entry.ObjectSignInName, + $entry.ObjectId, + $entry.ObjectType, + $entry.AssignmentType, + $entry.AssignmentInheritFrom, + $entry.GroupMembersCount, + $entry.RoleAssignmentPIMRelated, + $entry.RoleAssignmentPIMAssignmentType, + $entry.RoleAssignmentPIMAssignmentSlotStart, + $entry.RoleAssignmentPIMAssignmentSlotEnd, + $entry.RoleAssignmentId, + #($entry.RbacRelatedPolicyAssignment -replace '<', '<' -replace '>', '>'), + $entry.RbacRelatedPolicyAssignment, + $entry.CreatedOn, + $entry.CreatedBy + ) + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingRoleAssignments) + [void]$htmlTenantSummary.AppendLine(@" + +
ScopeRoleRole IdRole TypeDataIdentity DisplaynameIdentity SignInNameIdentity ObjectIdIdentity TypeApplicabilityApplies through membership Group DetailsPIMPIM assignment typePIM startPIM endRole AssignmentIdRelated Policy Assignment $noteOrNotCreatedOnCreatedBy
{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}{16}{17}{18}{19}
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$customRoleDefinitionsCreatedOrUpdatedCount Created/Updated custom Role definitions

+"@) + } + #endregion ChangeTrackingRoleAssignments + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + + #endregion ctrbac + + if ($azAPICallConf['htParameters'].NoResources -eq $false) { + #region ctresources + [void]$htmlTenantSummary.AppendLine(@" + +
+"@) + + #region ChangeTrackingResources + if ($resourcesCreatedOrChangedCount -gt 0) { + $tfCount = $resourcesCreatedOrChangedGroupedCount + $htmlTableId = 'TenantSummary_ChangeTrackingResources' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + + + + +"@) + $htmlSUMMARYChangeTrackingResources = $null + $htmlSUMMARYChangeTrackingResources = foreach ($entry in $resourcesCreatedOrChangedGrouped) { + $createdAndChanged = $entry.group.where( { $_.createdTime -gt $xdaysAgo -and $_.changedTime -gt $xdaysAgo }) + $createdAndChangedCount = $createdAndChanged.Count + $createdAndChangedInSubscriptionsCount = ($createdAndChanged | Group-Object -Property subscriptionId | Measure-Object).Count + + $created = $entry.group.where( { $_.createdTime -gt $xdaysAgo }) + $createdCount = $created.Count + $createdInSubscriptionsCount = ($created | Group-Object -Property subscriptionId | Measure-Object).Count + + $changed = $entry.group.where( { $_.changedTime -gt $xdaysAgo }) + $changedCount = $changed.Count + $changedInSubscriptionsCount = ($changed | Group-Object -Property subscriptionId | Measure-Object).Count + + @" + + + + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYChangeTrackingResources) + [void]$htmlTenantSummary.AppendLine(@" + +
ResourceTypeResource CountCreated&ChangedCreated&Changed SubsCreatedCreated SubsChangedChanged Subs
$($entry.Name)$($entry.Count)$($createdAndChangedCount)$($createdAndChangedInSubscriptionsCount)$($createdCount)$($createdInSubscriptionsCount)$($changedCount)$($changedInSubscriptionsCount)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

$resourcesCreatedOrChangedCount Created/Changed Resources

+"@) + } + #endregion ChangeTrackingResources + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + + #endregion ctresources + } + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + + $endChangeTracking = Get-Date + Write-Host " ChangeTracking duration: $((New-TimeSpan -Start $startChangeTracking -End $endChangeTracking).TotalMinutes) minutes ($((New-TimeSpan -Start $startChangeTracking -End $endChangeTracking).TotalSeconds) seconds)" + #endregion tenantSummaryChangeTracking + + showMemoryUsage + + #region tenantSummaryNaming + [void]$htmlTenantSummary.AppendLine(@' + +
+'@) + + $startSUMMARYNaming = Get-Date + Write-Host ' processing TenantSummary Findings' + + + $namingPolicyCount = $htNamingValidation.Policy.values.count + if ($namingPolicyCount -gt 0) { + $tfCount = $namingPolicyCount + $htmlTableId = 'TenantSummary_NamingPolicy' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYNamingPolicy = $null + $cnter = 0 + $htmlSUMMARYNamingPolicy = foreach ($key in $htNamingValidation.Policy.Keys | Sort-Object) { + $id = $key -replace '<', '<' -replace '>', '>' + if ($htNamingValidation.Policy.($key).name) { + $name = $htNamingValidation.Policy.($key).name -replace '<', '<' -replace '>', '>' + $nameInvalidChars = $htNamingValidation.Policy.($key).nameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $name = '' + $nameInvalidChars = '' + } + + if ($htNamingValidation.Policy.($key).displayName) { + $displayName = $htNamingValidation.Policy.($key).displayName -replace '<', '<' -replace '>', '>' + $displayNameInvalidChars = $htNamingValidation.Policy.($key).displayNameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $displayName = '' + $displayNameInvalidChars = '' + } + + + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingPolicy) + [void]$htmlTenantSummary.AppendLine(@" + +
IdNameName Invalid charsDisplayNameDisplayName Invalid chars
$($id)$($name)$($nameInvalidChars)$($displayName)$($displayNameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Policy $($namingPolicyCount) Naming findings

+"@) + } + + $namingPolicySetCount = $htNamingValidation.PolicySet.values.count + if ($namingPolicySetCount -gt 0) { + $tfCount = $namingPolicySetCount + $htmlTableId = 'TenantSummary_NamingPolicySet' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYNamingPolicySet = $null + $cnter = 0 + $htmlSUMMARYNamingPolicySet = foreach ($key in $htNamingValidation.PolicySet.Keys | Sort-Object) { + $id = $key -replace '<', '<' -replace '>', '>' + if ($htNamingValidation.PolicySet.($key).name) { + $name = $htNamingValidation.PolicySet.($key).name -replace '<', '<' -replace '>', '>' + $nameInvalidChars = $htNamingValidation.PolicySet.($key).nameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $name = '' + $nameInvalidChars = '' + } + + if ($htNamingValidation.PolicySet.($key).displayName) { + $displayName = $htNamingValidation.PolicySet.($key).displayName -replace '<', '<' -replace '>', '>' + $displayNameInvalidChars = $htNamingValidation.PolicySet.($key).displayNameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $displayName = '' + $displayNameInvalidChars = '' + } + + + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingPolicySet) + [void]$htmlTenantSummary.AppendLine(@" + +
IdNameName Invalid charsDisplayNameDisplayName Invalid chars
$($id)$($name)$($nameInvalidChars)$($displayName)$($displayNameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

PolicySet $($namingPolicySetCount) Naming findings

+"@) + } + + $namingPolicyAssignmentCount = $htNamingValidation.PolicyAssignment.values.count + if ($namingPolicyAssignmentCount -gt 0) { + $tfCount = $namingPolicyAssignmentCount + $htmlTableId = 'TenantSummary_NamingPolicyAssignment' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + + + +"@) + $htmlSUMMARYNamingPolicyAssignment = $null + $cnter = 0 + $htmlSUMMARYNamingPolicyAssignment = foreach ($key in $htNamingValidation.PolicyAssignment.Keys | Sort-Object) { + $id = $key -replace '<', '<' -replace '>', '>' + if ($htNamingValidation.PolicyAssignment.($key).name) { + $name = $htNamingValidation.PolicyAssignment.($key).name -replace '<', '<' -replace '>', '>' + $nameInvalidChars = $htNamingValidation.PolicyAssignment.($key).nameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $name = '' + $nameInvalidChars = '' + } + + if ($htNamingValidation.PolicyAssignment.($key).displayName) { + $displayName = $htNamingValidation.PolicyAssignment.($key).displayName -replace '<', '<' -replace '>', '>' + $displayNameInvalidChars = $htNamingValidation.PolicyAssignment.($key).displayNameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $displayName = '' + $displayNameInvalidChars = '' + } + + + @" + + + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingPolicyAssignment) + [void]$htmlTenantSummary.AppendLine(@" + +
IdNameName Invalid charsDisplayNameDisplayName Invalid chars
$($id)$($name)$($nameInvalidChars)$($displayName)$($displayNameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Policy assignment $($namingPolicyAssignmentCount) Naming findings

+"@) + } + + $namingManagementGroupCount = $htNamingValidation.ManagementGroup.values.count + if ($namingManagementGroupCount -gt 0) { + $tfCount = $namingManagementGroupCount + $htmlTableId = 'TenantSummary_NamingManagementGroup' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYNamingManagementGroup = $null + $cnter = 0 + $htmlSUMMARYNamingManagementGroup = foreach ($key in $htNamingValidation.ManagementGroup.Keys | Sort-Object) { + $id = $key -replace '<', '<' -replace '>', '>' + if ($htNamingValidation.ManagementGroup.($key).name) { + $name = $htNamingValidation.ManagementGroup.($key).name -replace '<', '<' -replace '>', '>' + $nameInvalidChars = $htNamingValidation.ManagementGroup.($key).nameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $name = '' + $nameInvalidChars = '' + } + + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingManagementGroup) + [void]$htmlTenantSummary.AppendLine(@" + +
IdNameName Invalid chars
$($id)$($name)$($nameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Management Group $($namingManagementGroupCount) Naming findings

+"@) + } + + + $namingSubscriptionCount = $htNamingValidation.Subscription.values.count + if ($namingSubscriptionCount -gt 0) { + $tfCount = $namingSubscriptionCount + $htmlTableId = 'TenantSummary_NamingSubscription' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYNamingSubscription = $null + $htmlSUMMARYNamingSubscription = foreach ($key in $htNamingValidation.Subscription.Keys | Sort-Object) { + + if ($htNamingValidation.Subscription.($key).displayName) { + $displayName = $htNamingValidation.Subscription.($key).displayName -replace '<', '<' -replace '>', '>' + $displayNameInvalidChars = $htNamingValidation.Subscription.($key).displayNameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $displayName = '' + $displayNameInvalidChars = '' + } + + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingSubscription) + [void]$htmlTenantSummary.AppendLine(@" + +
IdDisplayNameDisplayName Invalid chars
$($key)$($displayName)$($displayNameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

Subscription $($namingSubscriptionCount) Naming findings

+"@) + } + + + $namingRoleCount = $htNamingValidation.Role.values.count + if ($namingRoleCount -gt 0) { + $tfCount = $namingRoleCount + $htmlTableId = 'TenantSummary_NamingRole' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma + + + + + + + + + +"@) + $htmlSUMMARYNamingRole = $null + $htmlSUMMARYNamingRole = foreach ($key in $htNamingValidation.Role.Keys | Sort-Object) { + + if ($htNamingValidation.Role.($key).roleName) { + $roleName = $htNamingValidation.Role.($key).roleName -replace '<', '<' -replace '>', '>' + $roleNameInvalidChars = $htNamingValidation.Role.($key).roleNameInvalidChars -replace '<', '<' -replace '>', '>' + } + else { + $roleName = '' + $roleNameInvalidChars = '' + } + + @" + + + + + +"@ + } + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYNamingRole) + [void]$htmlTenantSummary.AppendLine(@" + +
IdNameName Invalid chars
$($key)$($roleName)$($roleNameInvalidChars)
+
+ + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@" +

RBAC $($namingRoleCount) Naming Findings

+"@) + } + + $endSUMMARYNaming = Get-Date + Write-Host " SUMMARYMGs duration: $((New-TimeSpan -Start $startSUMMARYNaming -End $endSUMMARYNaming).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYNaming -End $endSUMMARYNaming).TotalSeconds) seconds)" + + [void]$htmlTenantSummary.AppendLine(@' +
+'@) + #endregion tenantSummaryNaming + + $script:html += $htmlTenantSummary + $htmlTenantSummary = $null + $script:html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force + $script:html = $null +} +function removeInvalidFileNameChars { + param( + [Parameter(Mandatory = $true, + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [String]$Name + ) + if ($Name -like '`[Deprecated`]:*') { + $Name = $Name -replace '\[Deprecated\]\:', '[Deprecated]' + } + if ($Name -like '`[Preview`]:*') { + $Name = $Name -replace '\[Preview\]\:', '[Preview]' + } + if ($Name -like '`[ASC Private Preview`]:*') { + $Name = $Name -replace '\[ASC Private Preview\]\:', '[ASC Private Preview]' + } + return ($Name -replace ':', '_' -replace '/', '_' -replace '\\', '_' -replace '<', '_' -replace '>', '_' -replace '\*', '_' -replace '\?', '_' -replace '\|', '_' -replace '"', '_') +} +function ResolveObjectIds { + [CmdletBinding()]Param( + [object] + $objectIds, + + [switch] + $showActivity + ) + + $arrayObjectIdsToCheck = @() + $arrayObjectIdsToCheck = foreach ($objectToCheckIfAlreadyResolved in $objectIds) { + if (-not $htPrincipals.($objectToCheckIfAlreadyResolved)) { + $objectToCheckIfAlreadyResolved + } + else { + #Write-Host "$objectToCheckIfAlreadyResolved already resolved" + } + } + + if ($arrayObjectIdsToCheck.Count -gt 0) { + + $counterBatch = [PSCustomObject] @{ Value = 0 } + $batchSize = 1000 + $ObjectBatch = $arrayObjectIdsToCheck | Group-Object -Property { [math]::Floor($counterBatch.Value++ / $batchSize) } + $ObjectBatchCount = ($ObjectBatch | Measure-Object).Count + $batchCnt = 0 + + foreach ($batch in $ObjectBatch) { + $batchCnt++ + $objectsToProcess = '"{0}"' -f ($batch.Group.where({ testGuid $_ }) -join '","') + $currentTask = " Resolving ObjectIds - Batch #$batchCnt/$($ObjectBatchCount) ($(($batch.Group).Count))" + if ($showActivity) { + Write-Host $currentTask + } + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/directoryObjects/getByIds" + $method = 'POST' + $body = @" + { + "ids":[$($objectsToProcess)] + } +"@ + $resolveObjectIds = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -body $body -currentTask $currentTask + + foreach ($identity in $resolveObjectIds) { + if (-not $htPrincipals.($identity.id)) { + $arrayIdentityObject = [System.Collections.ArrayList]@() + if ($identity.'@odata.type' -eq '#microsoft.graph.user') { + if ($identity.userType -eq 'Guest') { + $script:htUserTypesGuest.($identity.id) = @{} + $script:htUserTypesGuest.($identity.id).userType = 'Guest' + } + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'User' + userType = $identity.userType + id = $identity.id + displayName = $identity.displayName + signInName = $identity.userPrincipalName + }) + } + if ($identity.'@odata.type' -eq '#microsoft.graph.group') { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'Group' + id = $identity.id + displayName = $identity.displayName + }) + } + if ($identity.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + if ($identity.servicePrincipalType -eq 'Application') { + if ($identity.appOwnerOrganizationId -eq $azAPICallConf['checkContext'].Tenant.Id) { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = 'SP APP INT' + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + else { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = 'SP APP EXT' + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + } + elseif ($identity.servicePrincipalType -eq 'ManagedIdentity') { + $miType = 'unknown' + if ($identity.alternativeNames) { + foreach ($altName in $identity.alternativeNames) { + if ($altName -like 'isExplicit=*') { + $splitAltName = $altName.split('=') + if ($splitAltName[1] -eq 'true') { + $miType = 'Usr' + } + if ($splitAltName[1] -eq 'false') { + $miType = 'Sys' + } + } + } + } + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'ServicePrincipal' + spTypeConcatinated = "SP MI $miType" + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + else { + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'servicePrincipal' + spTypeConcatinated = "SP $($identity.servicePrincipalType)" + servicePrincipalType = $identity.servicePrincipalType + id = $identity.id + appid = $identity.appId + displayName = $identity.displayName + appOwnerOrganizationId = $identity.appOwnerOrganizationId + alternativeNames = $identity.alternativeNames + }) + } + if (-not $htServicePrincipals.($identity.id)) { + $script:htServicePrincipals.($identity.id) = @{} + $script:htServicePrincipals.($identity.id) = $arrayIdentityObject + } + } + if (-not $htPrincipals.($identity.id)) { + $script:htPrincipals.($identity.id) = $arrayIdentityObject + } + } + } + if ($batch.Group.Count -ne $resolveObjectIds.Count) { + foreach ($objectId in $batch.Group) { + if ($resolveObjectIds.id -notcontains $objectId) { + if (-not $htPrincipals.($objectId)) { + $arrayIdentityObject = [System.Collections.ArrayList]@() + $null = $arrayIdentityObject.Add([PSCustomObject]@{ + type = 'Unknown' + id = $objectId + }) + $script:htPrincipals.($objectId) = $arrayIdentityObject + } + else { + #Write-Host "$($objectId) was already collected" + } + } + } + } + } + } +} +function runInfo { + #region RunInfo + Write-Host 'Run Info:' + if ($HierarchyMapOnly) { + Write-Host ' Creating HierarchyMap only' -ForegroundColor Green + } + else { + $script:paramsUsed = $Null + $startTimeUTC = ((Get-Date).ToUniversalTime()).ToString('dd-MMM-yyyy HH:mm:ss') + $script:paramsUsed += "Date: $startTimeUTC (UTC); Version: $ProductVersion " + + if ($azAPICallConf['htParameters'].accountType -eq 'ServicePrincipal') { + $script:paramsUsed += "ExecutedBy: $($azAPICallConf['checkContext'].Account.Id) (App/ClientId) ($($azAPICallConf['htParameters'].accountType)) " + } + elseif ($azAPICallConf['htParameters'].accountType -eq 'ManagedService') { + $script:paramsUsed += "ExecutedBy: $($azAPICallConf['checkContext'].Account.Id) (Id) ($($azAPICallConf['htParameters'].accountType)) " + } + elseif ($azAPICallConf['htParameters'].accountType -eq 'ClientAssertion') { + $script:paramsUsed += "ExecutedBy: $($azAPICallConf['checkContext'].Account.Id) (App/ClientId) ($($azAPICallConf['htParameters'].accountType)) " + } + else { + $script:paramsUsed += "ExecutedBy: $($azAPICallConf['checkContext'].Account.Id) ($($azAPICallConf['htParameters'].accountType), $($azAPICallConf['htParameters'].userType)) " + } + #$script:paramsUsed += "ManagementGroupId: $($ManagementGroupId) " + $script:paramsUsed += 'HierarchyMapOnly: false ' + Write-Host " Creating HierarchyMap, TenantSummary, DefinitionInsights and ScopeInsights - use parameter: '-HierarchyMapOnly' to only create the HierarchyMap" -ForegroundColor Yellow + + if ($azAPICallConf['htParameters'].ManagementGroupsOnly) { + Write-Host " Management Groups only = $($azAPICallConf['htParameters'].ManagementGroupsOnly)" -ForegroundColor Green + } + else { + Write-Host " Management Groups only = $($azAPICallConf['htParameters'].ManagementGroupsOnly) - use parameter -ManagementGroupsOnly to only collect data for Management Groups" -ForegroundColor Yellow + } + + if (($SubscriptionQuotaIdWhitelist).count -eq 1 -and $SubscriptionQuotaIdWhitelist[0] -eq 'undefined') { + Write-Host " Subscription Whitelist disabled - use parameter: '-SubscriptionQuotaIdWhitelist' to whitelist QuotaIds" -ForegroundColor Yellow + $script:paramsUsed += 'SubscriptionQuotaIdWhitelist: false ' + } + else { + Write-Host ' Subscription Whitelist enabled. Azure Governance Visualizer will only process Subscriptions where QuotaId startswith one of the following strings:' -ForegroundColor Green + foreach ($quotaIdFromSubscriptionQuotaIdWhitelist in $SubscriptionQuotaIdWhitelist) { + Write-Host " - $($quotaIdFromSubscriptionQuotaIdWhitelist)" -ForegroundColor Green + } + foreach ($whiteListEntry in $SubscriptionQuotaIdWhitelist) { + if ($whiteListEntry -eq 'undefined') { + Write-Host "When defining the 'SubscriptionQuotaIdWhitelist' make sure to remove the 'undefined' entry from the array :)" -ForegroundColor Red + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + } + $script:paramsUsed += "SubscriptionQuotaIdWhitelist: $($SubscriptionQuotaIdWhitelist -join ', ') " + } + + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $true) { + Write-Host " Microsoft Defender for Cloud Secure Score disabled (-NoMDfCSecureScore = $($azAPICallConf['htParameters'].NoMDfCSecureScore))" -ForegroundColor Green + $script:paramsUsed += 'NoMDfCSecureScore: true ' + } + else { + Write-Host " Microsoft Defender for Cloud Secure Score enabled - use parameter: '-NoMDfCSecureScore' to disable" -ForegroundColor Yellow + $script:paramsUsed += 'NoMDfCSecureScore: false ' + } + + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $true) { + Write-Host " Scrub Identity information for identityType='User' enabled (-DoNotShowRoleAssignmentsUserData = $($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData))" -ForegroundColor Green + $script:paramsUsed += 'DoNotShowRoleAssignmentsUserData: true ' + } + else { + Write-Host " Scrub Identity information for identityType='User' disabled - use parameter: '-DoNotShowRoleAssignmentsUserData' to scrub information such as displayName and signInName (email) for identityType='User'" -ForegroundColor Yellow + $script:paramsUsed += 'DoNotShowRoleAssignmentsUserData: false ' + } + + if ($LimitCriticalPercentage -eq 80) { + Write-Host " ARM Limits warning set to 80% (default) - use parameter: '-LimitCriticalPercentage' to set warning level accordingly" -ForegroundColor Yellow + #$script:paramsUsed += "LimitCriticalPercentage: 80% (default) " + } + else { + Write-Host " ARM Limits warning set to $($LimitCriticalPercentage)% (custom)" -ForegroundColor Green + #$script:paramsUsed += "LimitCriticalPercentage: $($LimitCriticalPercentage)% " + } + + if ($azAPICallConf['htParameters'].NoPolicyComplianceStates -eq $false) { + Write-Host " Policy States enabled - use parameter: '-NoPolicyComplianceStates' to disable Policy States" -ForegroundColor Yellow + $script:paramsUsed += 'NoPolicyComplianceStates: false ' + } + else { + Write-Host " Policy States disabled (-NoPolicyComplianceStates = $($azAPICallConf['htParameters'].NoPolicyComplianceStates))" -ForegroundColor Green + $script:paramsUsed += 'NoPolicyComplianceStates: true ' + } + + if (-not $NoResourceDiagnosticsPolicyLifecycle) { + Write-Host " Resource Diagnostics Policy Lifecycle recommendations enabled - use parameter: '-NoResourceDiagnosticsPolicyLifecycle' to disable Resource Diagnostics Policy Lifecycle recommendations" -ForegroundColor Yellow + $script:paramsUsed += 'NoResourceDiagnosticsPolicyLifecycle: false ' + } + else { + Write-Host " Resource Diagnostics Policy Lifecycle disabled (-NoResourceDiagnosticsPolicyLifecycle = $($NoResourceDiagnosticsPolicyLifecycle))" -ForegroundColor Green + $script:paramsUsed += 'NoResourceDiagnosticsPolicyLifecycle: true ' + } + + if (-not $NoAADGroupsResolveMembers) { + Write-Host " AAD Groups resolve members enabled (honors parameter -DoNotShowRoleAssignmentsUserData) - use parameter: '-NoAADGroupsResolveMembers' to disable resolving AAD Group memberships" -ForegroundColor Yellow + $script:paramsUsed += 'NoAADGroupsResolveMembers: false ' + if ($AADGroupMembersLimit -eq 500) { + Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Yellow + $script:paramsUsed += "AADGroupMembersLimit: $AADGroupMembersLimit " + } + else { + Write-Host " AADGroupMembersLimit = $AADGroupMembersLimit" -ForegroundColor Green + $script:paramsUsed += "AADGroupMembersLimit: $AADGroupMembersLimit " + } + } + else { + Write-Host " AAD Groups resolve members disabled (-NoAADGroupsResolveMembers = $($NoAADGroupsResolveMembers))" -ForegroundColor Green + $script:paramsUsed += 'NoAADGroupsResolveMembers: true ' + } + + Write-Host " AADServicePrincipalExpiryWarningDays: $AADServicePrincipalExpiryWarningDays" -ForegroundColor Yellow + #$script:paramsUsed += "AADServicePrincipalExpiryWarningDays: $AADServicePrincipalExpiryWarningDays " + + if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { + if (-not $AzureConsumptionPeriod -is [int]) { + Write-Host 'parameter -AzureConsumptionPeriod must be an integer' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + elseif ($AzureConsumptionPeriod -eq 0) { + Write-Host 'parameter -AzureConsumptionPeriod must be gt 0' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + else { + #$azureConsumptionStartDate = ((Get-Date).AddDays( - ($($AzureConsumptionPeriod)))).ToString("yyyy-MM-dd") + #$azureConsumptionEndDate = ((Get-Date).AddDays(-1)).ToString("yyyy-MM-dd") + + if ($AzureConsumptionPeriod -eq 1) { + Write-Host " Azure Consumption reporting enabled: $AzureConsumptionPeriod days (default) ($azureConsumptionStartDate - $azureConsumptionEndDate) - use parameter: '-AzureConsumptionPeriod' to define the period (days)" -ForegroundColor Yellow + } + else { + Write-Host " Azure Consumption reporting enabled: $AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)" -ForegroundColor Green + } + + if (-not $NoAzureConsumptionReportExportToCSV) { + Write-Host " Azure Consumption report export to CSV enabled - use parameter: '-NoAzureConsumptionReportExportToCSV' to disable" -ForegroundColor Yellow + } + else { + Write-Host " Azure Consumption report export to CSV disabled (-NoAzureConsumptionReportExportToCSV = $($NoAzureConsumptionReportExportToCSV))" -ForegroundColor Green + } + $script:paramsUsed += "DoAzureConsumption: true ($AzureConsumptionPeriod days ($azureConsumptionStartDate - $azureConsumptionEndDate)) " + $script:paramsUsed += "NoAzureConsumptionReportExportToCSV: $NoAzureConsumptionReportExportToCSV " + } + } + else { + Write-Host " Azure Consumption reporting disabled (-DoAzureConsumption = $($azAPICallConf['htParameters'].DoAzureConsumption))" -ForegroundColor Green + $script:paramsUsed += 'DoAzureConsumption: false ' + } + + if ($NoScopeInsights) { + Write-Host " ScopeInsights will not be created (-NoScopeInsights = $($NoScopeInsights))" -ForegroundColor Green + $script:paramsUsed += 'NoScopeInsights: true ' + } + else { + Write-Host " ScopeInsights will be created (-NoScopeInsights = $($NoScopeInsights)) Q: Why would you not want to show ScopeInsights? A: In larger tenants ScopeInsights may blow up the html file (up to unusable due to html file size)" -ForegroundColor Yellow + $script:paramsUsed += 'NoScopeInsights: false ' + } + + if ($NoSingleSubscriptionOutput) { + Write-Host " No single Subscription output will not be created (-NoSingleSubscriptionOutput = $($NoSingleSubscriptionOutput))" -ForegroundColor Green + $script:paramsUsed += 'NoSingleSubscriptionOutput: true ' + } + else { + Write-Host " Single Subscription output will be created (-NoSingleSubscriptionOutput = $($NoSingleSubscriptionOutput))" -ForegroundColor Yellow + $script:paramsUsed += 'NoSingleSubscriptionOutput: false ' + } + + if ($azAPICallConf['htParameters'].NoResourceProvidersDetailed -eq $true) { + Write-Host " ResourceProvider Detailed for TenantSummary disabled (-NoResourceProvidersDetailed = $($azAPICallConf['htParameters'].NoResourceProvidersDetailed))" -ForegroundColor Green + $script:paramsUsed += "NoResourceProvidersDetailed: $($azAPICallConf['htParameters'].NoResourceProvidersDetailed) " + } + else { + Write-Host " ResourceProvider Detailed for TenantSummary enabled - use parameter: '-NoResourceProvidersDetailed' to disable" -ForegroundColor Yellow + $script:paramsUsed += "NoResourceProvidersDetailed: $($azAPICallConf['htParameters'].NoResourceProvidersDetailed) " + } + + if ($azAPICallConf['htParameters'].NoResourceProvidersAtAll -eq $true) { + Write-Host " ResourceProvider collection disabled (NoResourceProvidersAtAll = $($azAPICallConf['htParameters'].NoResourceProvidersAtAll))" -ForegroundColor Green + $script:paramsUsed += "NoResourceProvidersAtAll: $($azAPICallConf['htParameters'].NoResourceProvidersAtAll) " + } + else { + Write-Host " ResourceProvider collection enabled - use parameter: 'NoResourceProvidersAtAll' to disable" -ForegroundColor Yellow + $script:paramsUsed += "NoResourceProvidersAtAll: $($azAPICallConf['htParameters'].NoResourceProvidersAtAll) " + } + + if ($azAPICallConf['htParameters'].LargeTenant -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -or $azAPICallConf['htParameters'].RBACAtScopeOnly) { + if ($azAPICallConf['htParameters'].LargeTenant) { + Write-Host " TenantSummary Policy assignments and Role assignments will not include assignment information on scopes where assignment is inherited, ScopeInsights will not be created, ResourceProvidersDetailed will not be created (-LargeTenant = $($azAPICallConf['htParameters'].LargeTenant))" -ForegroundColor Green + $script:paramsUsed += "LargeTenant: $($azAPICallConf['htParameters'].LargeTenant) " + $script:paramsUsed += "LargeTenant -> PolicyAtScopeOnly: $($azAPICallConf['htParameters'].PolicyAtScopeOnly) " + $script:paramsUsed += "LargeTenant -> RBACAtScopeOnly: $($azAPICallConf['htParameters'].RBACAtScopeOnly) " + $script:paramsUsed += "LargeTenant -> NoScopeInsights: $($NoScopeInsights) " + $script:paramsUsed += "LargeTenant -> NoResourceProvidersDetailed: $($azAPICallConf['htParameters'].NoResourceProvidersDetailed) " + } + else { + Write-Host " TenantSummary LargeTenant disabled (-LargeTenant = $($azAPICallConf['htParameters'].LargeTenant)) Q: Why would you not want to enable -LargeTenant? A: In larger tenants showing the inheritance on each scope may blow up the html file (up to unusable due to html file size)" -ForegroundColor Yellow + $script:paramsUsed += "LargeTenant: $($azAPICallConf['htParameters'].LargeTenant) " + + if ($azAPICallConf['htParameters'].PolicyAtScopeOnly) { + Write-Host " TenantSummary Policy assignments will not include assignment information on scopes where assignment is inherited (PolicyAtScopeOnly = $($azAPICallConf['htParameters'].PolicyAtScopeOnly))" -ForegroundColor Green + $script:paramsUsed += "PolicyAtScopeOnly: $($azAPICallConf['htParameters'].PolicyAtScopeOnly) " + } + else { + Write-Host " TenantSummary Policy assignments will include assignment information on scopes where assignment is inherited (PolicyAtScopeOnly = $($azAPICallConf['htParameters'].PolicyAtScopeOnly))" -ForegroundColor Yellow + $script:paramsUsed += "PolicyAtScopeOnly: $($azAPICallConf['htParameters'].PolicyAtScopeOnly) " + } + + if ($azAPICallConf['htParameters'].RBACAtScopeOnly) { + Write-Host " TenantSummary Role assignments will not include assignment information on scopes where assignment is inherited (RBACAtScopeOnly = $($azAPICallConf['htParameters'].RBACAtScopeOnly))" -ForegroundColor Green + $script:paramsUsed += "RBACAtScopeOnly: $($azAPICallConf['htParameters'].RBACAtScopeOnly) " + } + else { + Write-Host " TenantSummary Role assignments will include assignment information on scopes where assignment is inherited (RBACAtScopeOnly = $($azAPICallConf['htParameters'].RBACAtScopeOnly))" -ForegroundColor Yellow + $script:paramsUsed += "RBACAtScopeOnly: $($azAPICallConf['htParameters'].RBACAtScopeOnly) " + } + } + } + else { + Write-Host " TenantSummary LargeTenant disabled (-LargeTenant = $($azAPICallConf['htParameters'].LargeTenant)) Q: Why would you not want to enable -LargeTenant? A: In larger tenants showing the inheritance on each scope may blow up the html file (up to unusable due to html file size)" -ForegroundColor Yellow + $script:paramsUsed += "LargeTenant: $($azAPICallConf['htParameters'].LargeTenant) " + + if ($azAPICallConf['htParameters'].PolicyAtScopeOnly) { + Write-Host " TenantSummary Policy assignments will not include assignment information on scopes where assignment is inherited (PolicyAtScopeOnly = $($azAPICallConf['htParameters'].PolicyAtScopeOnly))" -ForegroundColor Green + $script:paramsUsed += "PolicyAtScopeOnly: $($azAPICallConf['htParameters'].PolicyAtScopeOnly) " + } + else { + Write-Host " TenantSummary Policy assignments will include assignment information on scopes where assignment is inherited (PolicyAtScopeOnly = $($azAPICallConf['htParameters'].PolicyAtScopeOnly))" -ForegroundColor Yellow + $script:paramsUsed += "PolicyAtScopeOnly: $($azAPICallConf['htParameters'].PolicyAtScopeOnly) " + } + + if ($azAPICallConf['htParameters'].RBACAtScopeOnly) { + Write-Host " TenantSummary Role assignments will not include assignment information on scopes where assignment is inherited (RBACAtScopeOnly = $($azAPICallConf['htParameters'].RBACAtScopeOnly))" -ForegroundColor Green + $script:paramsUsed += "RBACAtScopeOnly: $($azAPICallConf['htParameters'].RBACAtScopeOnly) " + } + else { + Write-Host " TenantSummary Role assignments will include assignment information on scopes where assignment is inherited (RBACAtScopeOnly = $($azAPICallConf['htParameters'].RBACAtScopeOnly))" -ForegroundColor Yellow + $script:paramsUsed += "RBACAtScopeOnly: $($azAPICallConf['htParameters'].RBACAtScopeOnly) " + } + } + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + Write-Host " TenantSummary Policy assignments will also include assignments on ResourceGroups (DoNotIncludeResourceGroupsOnPolicy = $($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy))" -ForegroundColor Yellow + $script:paramsUsed += 'DoNotIncludeResourceGroupsOnPolicy: false ' + } + else { + Write-Host " TenantSummary Policy assignments will not include assignments on ResourceGroups (DoNotIncludeResourceGroupsOnPolicy = $($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy))" -ForegroundColor Green + $script:paramsUsed += 'DoNotIncludeResourceGroupsOnPolicy: true ' + } + + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + Write-Host " TenantSummary RBAC Role assignments will also include assignments on ResourceGroups and Resources (DoNotIncludeResourceGroupsAndResourcesOnRBAC = $($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC))" -ForegroundColor Yellow + $script:paramsUsed += 'DoNotIncludeResourceGroupsAndResourcesOnRBAC: false ' + } + else { + Write-Host " TenantSummary RBAC Role assignments will not include assignments on ResourceGroups and Resources (DoNotIncludeResourceGroupsAndResourcesOnRBAC = $($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC))" -ForegroundColor Green + $script:paramsUsed += 'DoNotIncludeResourceGroupsAndResourcesOnRBAC: true ' + } + + if (-not $NoCsvExport) { + Write-Host " CSV Export enabled: enriched 'Role assignments' data, enriched 'Policy assignments' data and 'all resources' (subscriptionId, mgPath, resourceType, id, name, location, tags, createdTime, changedTime) (-NoCsvExport = $($NoCsvExport))" -ForegroundColor Yellow + $script:paramsUsed += 'NoCsvExport: false ' + } + else { + Write-Host " CSV Export disabled: enriched 'Role assignments' data, enriched 'Policy assignments' data and 'all resources' (subscriptionId, mgPath, resourceType, id, name, location, tags, createdTime, changedTime) (-NoCsvExport = $($NoCsvExport))" -ForegroundColor Green + $script:paramsUsed += 'NoCsvExport: true ' + } + + if (-not $azAPICallConf['htParameters'].NoJsonExport) { + Write-Host " JSON Export enabled: export of ManagementGroup Hierarchy including all MG/Sub Policy/RBAC definitions, Policy/RBAC assignments and some more relevant information to JSON (-NoJsonExport = $($azAPICallConf['htParameters'].NoJsonExport))" -ForegroundColor Yellow + $script:paramsUsed += 'NoJsonExport: false ' + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) { + if (-not $JsonExportExcludeResourceGroups) { + Write-Host " JSON Export will also include Policy assignments on ResourceGroups (JsonExportExcludeResourceGroups = $($JsonExportExcludeResourceGroups))" -ForegroundColor Yellow + $script:paramsUsed += "JsonExportExcludeResourceGroups Policy: $($JsonExportExcludeResourceGroups) " + } + else { + Write-Host " JSON Export will not include Policy assignments on ResourceGroups (JsonExportExcludeResourceGroups = $($JsonExportExcludeResourceGroups))" -ForegroundColor Green + $script:paramsUsed += "JsonExportExcludeResourceGroups Policy: $($JsonExportExcludeResourceGroups) " + } + } + if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) { + if (-not $JsonExportExcludeResourceGroups) { + Write-Host " JSON Export will also include Role assignments on ResourceGroups (JsonExportExcludeResourceGroups = $($JsonExportExcludeResourceGroups))" -ForegroundColor Yellow + $script:paramsUsed += "JsonExportExcludeResourceGroups RBAC: $($JsonExportExcludeResourceGroups) " + + } + else { + Write-Host " JSON Export will not include Role assignments on ResourceGroups (JsonExportExcludeResourceGroups = $($JsonExportExcludeResourceGroups))" -ForegroundColor Green + $script:paramsUsed += "JsonExportExcludeResourceGroups RBAC: $($JsonExportExcludeResourceGroups) " + } + if (-not $JsonExportExcludeResources) { + Write-Host " JSON Export will also include Role assignments on Resources (JsonExportExcludeResources = $($JsonExportExcludeResources))" -ForegroundColor Yellow + $script:paramsUsed += "JsonExportExcludeResources RBAC: $($JsonExportExcludeResources) " + } + else { + Write-Host " JSON Export will not include Role assignments on Resources (JsonExportExcludeResources = $($JsonExportExcludeResources))" -ForegroundColor Green + $script:paramsUsed += "JsonExportExcludeResources RBAC: $($JsonExportExcludeResources) " + } + } + } + else { + Write-Host " JSON Export disabled: export of ManagementGroup Hierarchy including all MG/Sub Policy/RBAC definitions, Policy/RBAC assignments and some more relevant information to JSON (-NoJsonExport = $($azAPICallConf['htParameters'].NoJsonExport))" -ForegroundColor Green + $script:paramsUsed += 'NoJsonExport: true ' + } + + if ($ThrottleLimit -eq 10) { + Write-Host " ThrottleLimit = $ThrottleLimit" -ForegroundColor Yellow + #$script:paramsUsed += "ThrottleLimit: $ThrottleLimit " + } + else { + Write-Host " ThrottleLimit = $ThrottleLimit" -ForegroundColor Green + #$script:paramsUsed += "ThrottleLimit: $ThrottleLimit " + } + + + if ($ChangeTrackingDays -eq 14) { + Write-Host " ChangeTrackingDays = $ChangeTrackingDays" -ForegroundColor Yellow + #$script:paramsUsed += "ChangeTrackingDays: $ChangeTrackingDays " + } + else { + Write-Host " ChangeTrackingDays = $ChangeTrackingDays" -ForegroundColor Green + #$script:paramsUsed += "ChangeTrackingDays: $ChangeTrackingDays " + } + + if ($azAPICallConf['htParameters'].NoResources) { + Write-Host " NoResources = $($azAPICallConf['htParameters'].NoResources)" -ForegroundColor Green + $script:paramsUsed += "NoResources: $($azAPICallConf['htParameters'].NoResources) " + } + else { + Write-Host " NoResources = $($azAPICallConf['htParameters'].NoResources)" -ForegroundColor Yellow + $script:paramsUsed += "NoResources: $($azAPICallConf['htParameters'].NoResources) " + } + + if ($ShowMemoryUsage) { + Write-Host " ShowMemoryUsage = $($ShowMemoryUsage)" -ForegroundColor Green + #$script:paramsUsed += "ShowMemoryUsage: $($ShowMemoryUsage) " + } + else { + Write-Host " ShowMemoryUsage = $($ShowMemoryUsage)" -ForegroundColor Yellow + #$script:paramsUsed += "ShowMemoryUsage: $($ShowMemoryUsage) " + } + + if ($CriticalMemoryUsage -ne 99) { + Write-Host " CriticalMemoryUsage = $($CriticalMemoryUsage)%" -ForegroundColor green + #$script:paramsUsed += "ShowMemoryUsage: $($ShowMemoryUsage) " + } + else { + Write-Host " CriticalMemoryUsage = $($CriticalMemoryUsage)%" -ForegroundColor Yellow + #$script:paramsUsed += "ShowMemoryUsage: $($ShowMemoryUsage) " + } + + if ($azAPICallConf['htParameters'].DoPSRule) { + Write-Host " DoPSRule = $($azAPICallConf['htParameters'].DoPSRule)" -ForegroundColor Green + $script:paramsUsed += "DoPSRule: $($azAPICallConf['htParameters'].DoPSRule) " + + if ($azAPICallConf['htParameters'].PSRuleFailedOnly) { + Write-Host " PSRuleFailedOnly = $($azAPICallConf['htParameters'].PSRuleFailedOnly)" -ForegroundColor Green + $script:paramsUsed += "PSRuleFailedOnly: $($azAPICallConf['htParameters'].PSRuleFailedOnly) " + } + else { + Write-Host " PSRuleFailedOnly = $($azAPICallConf['htParameters'].PSRuleFailedOnly)" -ForegroundColor Yellow + $script:paramsUsed += "PSRuleFailedOnly: $($azAPICallConf['htParameters'].PSRuleFailedOnly) " + } + } + else { + Write-Host " DoPSRule = $($azAPICallConf['htParameters'].DoPSRule)" -ForegroundColor Yellow + $script:paramsUsed += "DoPSRule: $($azAPICallConf['htParameters'].DoPSRule) " + } + + if ($NoPIMEligibility) { + Write-Host " NoPIMEligibility = $($NoPIMEligibility)" -ForegroundColor Green + $script:paramsUsed += "NoPIMEligibility: $($NoPIMEligibility) " + } + else { + Write-Host " NoPIMEligibility = $($NoPIMEligibility)" -ForegroundColor Yellow + $script:paramsUsed += "NoPIMEligibility: $($NoPIMEligibility) " + } + + if ($PIMEligibilityIgnoreScope) { + Write-Host " PIMEligibilityIgnoreScope = $($PIMEligibilityIgnoreScope)" -ForegroundColor Green + #$script:paramsUsed += "PIMEligibilityIgnoreScope: $($PIMEligibilityIgnoreScope) " + } + else { + Write-Host " PIMEligibilityIgnoreScope = $($PIMEligibilityIgnoreScope)" -ForegroundColor Yellow + #$script:paramsUsed += "PIMEligibilityIgnoreScope: $($PIMEligibilityIgnoreScope) " + } + + if ($NoPIMEligibilityIntegrationRoleAssignmentsAll) { + Write-Host " NoPIMEligibilityIntegrationRoleAssignmentsAll = $($NoPIMEligibilityIntegrationRoleAssignmentsAll)" -ForegroundColor Green + #$script:paramsUsed += "NoPIMEligibilityIntegrationRoleAssignmentsAll: $($NoPIMEligibilityIntegrationRoleAssignmentsAll) " + } + else { + Write-Host " NoPIMEligibilityIntegrationRoleAssignmentsAll = $($NoPIMEligibilityIntegrationRoleAssignmentsAll)" -ForegroundColor Yellow + #$script:paramsUsed += "NoPIMEligibilityIntegrationRoleAssignmentsAll: $($NoPIMEligibilityIntegrationRoleAssignmentsAll) " + } + + if ($NoDefinitionInsightsDedicatedHTML) { + Write-Host " NoDefinitionInsightsDedicatedHTML = $($NoDefinitionInsightsDedicatedHTML)" -ForegroundColor Green + #$script:paramsUsed += "NoDefinitionInsightsDedicatedHTML: $($NoDefinitionInsightsDedicatedHTML) " + } + else { + Write-Host " NoDefinitionInsightsDedicatedHTML = $($NoDefinitionInsightsDedicatedHTML)" -ForegroundColor Yellow + #$script:paramsUsed += "NoDefinitionInsightsDedicatedHTML: $($NoDefinitionInsightsDedicatedHTML) " + } + + if ($NoALZPolicyVersionChecker) { + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Green + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " + } + else { + Write-Host " NoALZPolicyVersionChecker = $($NoALZPolicyVersionChecker)" -ForegroundColor Yellow + #$script:paramsUsed += "NoALZPolicyVersionChecker: $($NoALZPolicyVersionChecker) " + } + + if ($NoStorageAccountAccessAnalysis) { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Green + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + } + else { + Write-Host " NoStorageAccountAccessAnalysis = $($NoStorageAccountAccessAnalysis)" -ForegroundColor Yellow + #$script:paramsUsed += "NoStorageAccountAccessAnalysis: $($NoStorageAccountAccessAnalysis) " + if ($StorageAccountAccessAnalysisSubscriptionTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisSubscriptionTags.Count -gt 0) { + Write-Host " StorageAccountAccessAnalysisSubscriptionTags: $($StorageAccountAccessAnalysisSubscriptionTags -join ', ')" -ForegroundColor Green + } + if ($StorageAccountAccessAnalysisStorageAccountTags[0] -ne 'undefined' -and $StorageAccountAccessAnalysisStorageAccountTags.Count -gt 0) { + Write-Host " StorageAccountAccessAnalysisStorageAccountTags: $($StorageAccountAccessAnalysisStorageAccountTags -join ', ')" -ForegroundColor Green + } + } + + if ($NoNetwork) { + Write-Host " NoNetwork = $($NoNetwork)" -ForegroundColor Green + #$script:paramsUsed += "NoNetwork: $($NoNetwork) " + } + else { + Write-Host " NoNetwork = $($NoNetwork)" -ForegroundColor Yellow + #$script:paramsUsed += "NoNetwork: $($NoNetwork) " + + if ($NetworkSubnetIPAddressUsageCriticalPercentage -ne 80) { + Write-Host " NetworkSubnetIPAddressUsageCriticalPercentage = $($NetworkSubnetIPAddressUsageCriticalPercentage)" -ForegroundColor Green + #$script:paramsUsed += "NetworkSubnetIPAddressUsageCriticalPercentage: $($NetworkSubnetIPAddressUsageCriticalPercentage) " + } + else { + Write-Host " NoNetwork = $($NetworkSubnetIPAddressUsageCriticalPercentage)" -ForegroundColor Yellow + #$script:paramsUsed += "NetworkSubnetIPAddressUsageCriticalPercentage: $($NetworkSubnetIPAddressUsageCriticalPercentage) " + } + } + + if ($GitHubActionsOIDC) { + Write-Host " GitHubActionsOIDC = $($GitHubActionsOIDC)" -ForegroundColor Green + #$script:paramsUsed += "GitHubActionsOIDC: $($GitHubActionsOIDC) " + } + else { + Write-Host " GitHubActionsOIDC = $($GitHubActionsOIDC)" -ForegroundColor Yellow + #$script:paramsUsed += "GitHubActionsOIDC: $($GitHubActionsOIDC) " + } + + } + #endregion RunInfo +} +function selectMg() { + Write-Host 'Please select a Management Group from the list below:' + $MgtGroupArray | Select-Object '#', Name, @{Name = 'displayName'; Expression = { $_.properties.displayName } }, Id | Format-Table + Write-Host "If you don't see your ManagementGroupID try using the parameter -ManagementGroupID" -ForegroundColor Yellow + if ($msg) { + Write-Host $msg -ForegroundColor Red + } + + $script:SelectedMG = Read-Host "Please enter a selection from 1 to $(($MgtGroupArray).count)" + + if ($SelectedMG -match '^[\d\.]+$') { + if ([int]$SelectedMG -lt 1 -or [int]$SelectedMG -gt ($MgtGroupArray).count) { + $msg = "last input '$SelectedMG' is out of range, enter a number from the selection!" + selectMg + } + } + else { + $msg = "last input '$SelectedMG' is not numeric, enter a number from the selection!" + selectMg + } +} +function setBaseVariablesMG { + if (($azAPICallConf['checkContext']).Tenant.Id -ne $ManagementGroupId) { + $script:mgSubPathTopMg = $selectedManagementGroupId.ParentName + $script:getMgParentId = $selectedManagementGroupId.ParentName + $script:getMgParentName = $selectedManagementGroupId.ParentDisplayName + $script:mermaidprnts = "'$(($azAPICallConf['checkContext']).Tenant.Id)',$getMgParentId" + } + else { + $script:hierarchyLevel = -1 + $script:mgSubPathTopMg = "$ManagementGroupId" + $script:getMgParentId = "'$ManagementGroupId'" + $script:getMgParentName = 'Tenant Root' + $script:mermaidprnts = "'$getMgParentId',$getMgParentId" + } +} +function setOutput { + if (-not [IO.Path]::IsPathRooted($outputPath)) { + $outputPath = Join-Path -Path (Get-Location).Path -ChildPath $outputPath + } + $outputPath = Join-Path -Path $outputPath -ChildPath '.' + $script:outputPath = [IO.Path]::GetFullPath($outputPath) + if (-not (Test-Path $outputPath)) { + Write-Host "path $outputPath does not exist - please create it!" -ForegroundColor Red + Throw 'Error - check the last console output for details' + } + else { + Write-Host "Output/Files will be created in path '$outputPath'" + } + + #fileTimestamp + try { + $script:fileTimestamp = (Get-Date -Format $FileTimeStampFormat) + } + catch { + Write-Host "fileTimestamp format: '$($FileTimeStampFormat)' invalid; continue with default format: 'yyyyMMdd_HHmmss'" -ForegroundColor Red + $FileTimeStampFormat = 'yyyyMMdd_HHmmss' + $script:fileTimestamp = (Get-Date -Format $FileTimeStampFormat) + } + + $script:executionDateTimeInternationalReadable = Get-Date -Format 'dd-MMM-yyyy HH:mm:ss' + $script:currentTimeZone = (Get-TimeZone).Id +} +function setTranscript { + if ($ManagementGroupId) { + if ($onAzureDevOpsOrGitHubActions -eq $true) { + if ($HierarchyMapOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ManagementGroupId)_Log.txt" + } + elseif ($ManagementGroupsOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ManagementGroupId)_Log.txt" + } + else { + $script:fileNameTranscript = "AzGovViz_$($ManagementGroupId)_Log.txt" + } + } + else { + if ($HierarchyMapOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" + } + elseif ($ManagementGroupsOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" + } + else { + $script:fileNameTranscript = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_$($ManagementGroupId)_Log.txt" + } + } + } + else { + if ($onAzureDevOpsOrGitHubActions -eq $true) { + if ($HierarchyMapOnly -eq $true) { + $script:fileNameTranscript = 'AzGovViz_HierarchyMapOnly_Log.txt' + } + elseif ($ManagementGroupsOnly -eq $true) { + $script:fileNameTranscript = 'AzGovViz_ManagementGroupsOnly_Log.txt' + } + else { + $script:fileNameTranscript = 'AzGovViz_Log.txt' + } + } + else { + if ($HierarchyMapOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_HierarchyMapOnly_$($ProductVersion)_$($fileTimestamp)_Log.txt" + } + elseif ($ManagementGroupsOnly -eq $true) { + $script:fileNameTranscript = "AzGovViz_ManagementGroupsOnly_$($ProductVersion)_$($fileTimestamp)_Log.txt" + } + else { + $script:fileNameTranscript = "AzGovViz_$($ProductVersion)_$($fileTimestamp)_Log.txt" + } + } + } + Write-Host "Writing transcript: $($outputPath)$($DirectorySeparatorChar)$($fileNameTranscript)" + Start-Transcript -Path "$($outputPath)$($DirectorySeparatorChar)$($fileNameTranscript)" +} +function showMemoryUsage { + + function makeDouble { + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)]$MemoryUsed + ) + + try { + $memoryUsedDouble = [double]($memoryUsed -replace ',', '.') + } + catch { + $memoryUsedDouble = [string]$MemoryUsed + } + return $memoryUsedDouble + } + + function getMemoryUsage { + if ($IsLinux) { + $memoryUsed = 100 - (free | grep Mem | awk '{print $4/$2 * 100.0}') + makeDouble $memoryUsed + } + if ($IsWindows) { + $memoryUsed = (Get-CimInstance win32_operatingsystem | ForEach-Object { '{0:N2}' -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) * 100) / $_.TotalVisibleMemorySize) }) + makeDouble $memoryUsed + } + } + $memoryUsed = getMemoryUsage + + if ($memoryUsed -is [double]) { + if ($memoryUsed -gt $CriticalMemoryUsage) { + Write-Host "System memory utilization HIGH: $([math]::Round($memoryUsed))%" -ForegroundColor Magenta + Write-Host 'Init garbage collection (GC)' + $PSMemoryBefore = [System.GC]::GetTotalMemory($false) + Write-Host " PS memory used before GC: $($PSMemoryBefore /1MB)MB ($PSMemoryBefore)" + $startGC = Get-Date + $PSMemoryAfter = [System.GC]::GetTotalMemory($true) + $endGC = Get-Date + $PSMemoryDiff = $PSMemoryBefore - $PSMemoryAfter + Write-Host " PS memory used after GC: $($PSMemoryAfter /1MB)MB ($PSMemoryAfter)" + Write-Host " GC cleared $($PSMemoryDiff /1MB)MB ($PSMemoryDiff)" -ForegroundColor Green + Write-Host " GC duration: $((New-TimeSpan -Start $startGC -End $endGC).TotalSeconds) seconds" + Write-Host " System memory utilization after GC: $(getMemoryUsage)%" + } + else { + if ($ShowMemoryUsage) { + Write-Host "System memory utilization: $([math]::Round($memoryUsed))%" + } + } + } + else { + Write-Host "System memory utilization: $($memoryUsed)% (not double)" + } +} +function stats { + #region Stats + if (-not $StatsOptOut) { + + $dur = 0 + if ($durationProduct.TotalMinutes -lt 5) { + $dur = 5 + } + + if ($azAPICallConf['htParameters'].onAzureDevOps) { + if ($env:BUILD_REPOSITORY_ID) { + $hashTenantIdOrRepositoryId = [string]($env:BUILD_REPOSITORY_ID) + } + else { + $hashTenantIdOrRepositoryId = [string]($azAPICallConf['checkContext'].Tenant.Id) + } + } + else { + $hashTenantIdOrRepositoryId = [string]($azAPICallConf['checkContext'].Tenant.Id) + } + + $hashAccId = [string]($azAPICallConf['checkContext'].Account.Id) + + $hasher384 = [System.Security.Cryptography.HashAlgorithm]::Create('sha384') + $hasher512 = [System.Security.Cryptography.HashAlgorithm]::Create('sha512') + + $hashTenantIdOrRepositoryIdSplit = $hashTenantIdOrRepositoryId.split('-') + $hashAccIdSplit = $hashAccId.split('-') + + if (($hashTenantIdOrRepositoryIdSplit[0])[0] -match '[a-z]') { + $hashTenantIdOrRepositoryIdUse = "$(($hashTenantIdOrRepositoryIdSplit[0]).substring(2))$($hashAccIdSplit[2])" + $hashTenantIdOrRepositoryIdUse = $hasher512.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashTenantIdOrRepositoryIdUse)) + $hashTenantIdOrRepositoryIdUse = "$(([System.BitConverter]::ToString($hashTenantIdOrRepositoryIdUse)) -replace '-')" + } + else { + $hashTenantIdOrRepositoryIdUse = "$(($hashTenantIdOrRepositoryIdSplit[4]).substring(6))$($hashAccIdSplit[1])" + $hashTenantIdOrRepositoryIdUse = $hasher384.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashTenantIdOrRepositoryIdUse)) + $hashTenantIdOrRepositoryIdUse = "$(([System.BitConverter]::ToString($hashTenantIdOrRepositoryIdUse)) -replace '-')" + } + + if (($hashAccIdSplit[0])[0] -match '[a-z]') { + $hashAccIdUse = "$($hashAccIdSplit[0].substring(2))$($hashTenantIdOrRepositoryIdSplit[2])" + $hashAccIdUse = $hasher512.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashAccIdUse)) + $hashAccIdUse = "$(([System.BitConverter]::ToString($hashAccIdUse)) -replace '-')" + $hashUse = "$($hashAccIdUse)$($hashTenantIdOrRepositoryIdUse)" + } + else { + $hashAccIdUse = "$($hashAccIdSplit[4].substring(6))$($hashTenantIdOrRepositoryIdSplit[1])" + $hashAccIdUse = $hasher384.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashAccIdUse)) + $hashAccIdUse = "$(([System.BitConverter]::ToString($hashAccIdUse)) -replace '-')" + $hashUse = "$($hashTenantIdOrRepositoryIdUse)$($hashAccIdUse)" + } + + $identifierBase = $hasher512.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashUse)) + $script:statsIdentifier = "$(([System.BitConverter]::ToString($identifierBase)) -replace '-')" + + $accountInfo = "$($azAPICallConf['htParameters'].accountType)$($azAPICallConf['htParameters'].userType)" + if ($azAPICallConf['htParameters'].accountType -eq 'ServicePrincipal' -or $azAPICallConf['htParameters'].accountType -eq 'ManagedService' -or $azAPICallConf['htParameters'].accountType -eq 'ClientAssertion') { + $accountInfo = $azAPICallConf['htParameters'].accountType + } + + $scopeUsage = 'childManagementGroup' + if ($ManagementGroupId -eq $azAPICallConf['checkContext'].Tenant.Id) { + $scopeUsage = 'rootManagementGroup' + } + + $statsCountSubscriptions = 'less than 100' + if (($htSubscriptionsMgPath.Keys).Count -ge 100) { + $statsCountSubscriptions = 'more than 100' + } + + $tryCounter = 0 + do { + if ($tryCounter -gt 0) { + Start-Sleep -Seconds ($tryCounter * 3) + } + $tryCounter++ + $statsSuccess = $true + try { + $statusBody = @" +{ + "name": "Microsoft.ApplicationInsights.Event", + "time": "$((Get-Date).ToUniversalTime())", + "iKey": "ffcd6b2e-1a5e-429f-9495-e3492decfe06", + "data": { + "baseType": "EventData", + "baseData": { + "name": "$($Product)", + "ver": 2, + "properties": { + "accType": "$($accountInfo)", + "azCloud": "$($azAPICallConf['checkContext'].Environment.Name)", + "identifier": "$($statsIdentifier)", + "platform": "$($azAPICallConf['htParameters'].CodeRunPlatform)", + "productVersion": "$($ProductVersion)", + "AzAPICallVersion": "$($AzAPICallVersion)", + "psAzAccountsVersion": "$($azAPICallConf['htParameters'].AzAccountsVersion)", + "psVersion": "$($PSVersionTable.PSVersion)", + "scopeUsage": "$($scopeUsage)", + "statsCountErrors": "$($Error.Count)", + "statsCountSubscriptions": "$($statsCountSubscriptions)", + "statsParametersDoNotIncludeResourceGroupsAndResourcesOnRBAC": "$($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC)", + "statsParametersDoNotIncludeResourceGroupsOnPolicy": "$($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy)", + "statsParametersDoNotShowRoleAssignmentsUserData": "$($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData)", + "statsParametersHierarchyMapOnly": "$HierarchyMapOnly", + "statsParametersManagementGroupsOnly": "$($azAPICallConf['htParameters'].ManagementGroupsOnly)", + "statsParametersLargeTenant": "$($azAPICallConf['htParameters'].LargeTenant)", + "statsParametersNoASCSecureScore": "$($azAPICallConf['htParameters'].NoMDfCSecureScore)", + "statsParametersDoAzureConsumption": "$($azAPICallConf['htParameters'].DoAzureConsumption)", + "statsParametersNoJsonExport": "$($azAPICallConf['htParameters'].NoJsonExport)", + "statsParametersNoScopeInsights": "$($NoScopeInsights)", + "statsParametersNoSingleSubscriptionOutput": "$($NoSingleSubscriptionOutput)", + "statsParametersNoPolicyComplianceStates": "$($azAPICallConf['htParameters'].NoPolicyComplianceStates)", + "statsParametersNoResourceProvidersDetailed": "$($azAPICallConf['htParameters'].NoResourceProvidersDetailed)", + "statsParametersNoResourceProvidersAtAll": "$($azAPICallConf['htParameters'].NoResourceProvidersAtAll)", + "statsParametersNoResources": "$($azAPICallConf['htParameters'].NoResources)", + "statsParametersPolicyAtScopeOnly": "$($azAPICallConf['htParameters'].PolicyAtScopeOnly)", + "statsParametersRBACAtScopeOnly": "$($azAPICallConf['htParameters'].RBACAtScopeOnly)", + "statsParametersDoPSRule": "$($azAPICallConf['htParameters'].DoPSRule)", + "statsParametersNoPIMEligibility": "$($NoPIMEligibility)", + "statsParametersNoALZPolicyVersionChecker": "$($NoALZPolicyVersionChecker)", + "statsParametersNoStorageAccountAccessAnalysis": "$($NoStorageAccountAccessAnalysis)", + "statsParametersNoNetwork": "$($NoNetwork)", + "statsTry": "$($tryCounter)", + "statsDurationProduct": "$($dur)" + } + } + } +} +"@ + $stats = Invoke-WebRequest -Uri 'https://dc.services.visualstudio.com/v2/track' -Method 'POST' -Body $statusBody + } + catch { + $statsSuccess = $false + } + } + until($statsSuccess -eq $true -or $tryCounter -gt 5) + } + else { + #noStats + $script:statsIdentifier = (New-Guid).Guid + $tryCounter = 0 + do { + if ($tryCounter -gt 0) { + Start-Sleep -Seconds ($tryCounter * 3) + } + $tryCounter++ + $statsSuccess = $true + try { + $statusBody = @" +{ + "name": "Microsoft.ApplicationInsights.Event", + "time": "$((Get-Date).ToUniversalTime())", + "iKey": "ffcd6b2e-1a5e-429f-9495-e3492decfe06", + "data": { + "baseType": "EventData", + "baseData": { + "name": "$($Product)", + "ver": 2, + "properties": { + "identifier": "$($statsIdentifier)", + "statsTry": "$($tryCounter)" + } + } + } +} +"@ + $stats = Invoke-WebRequest -Uri 'https://dc.services.visualstudio.com/v2/track' -Method 'POST' -Body $statusBody + } + catch { + $statsSuccess = $false + } + } + until($statsSuccess -eq $true -or $tryCounter -gt 5) + } + #endregion Stats +} +function testGuid { + [OutputType([bool])] + param + ( + [Parameter(Mandatory = $true)] + [string]$StringGuid + ) + + $ObjectGuid = [System.Guid]::empty + return [System.Guid]::TryParse($StringGuid, [System.Management.Automation.PSReference]$ObjectGuid) # Returns True if successfully parsed +} +function testPowerShellVersion { + + Write-Host 'Checking PowerShell edition and version' + $requiredPSVersion = '7.0.3' + $splitRequiredPSVersion = $requiredPSVersion.split('.') + $splitRequiredPSVersionMajor = $splitRequiredPSVersion[0] + $splitRequiredPSVersionMinor = $splitRequiredPSVersion[1] + $splitRequiredPSVersionPatch = $splitRequiredPSVersion[2] + + $thisPSVersion = ($PSVersionTable.PSVersion) + $thisPSVersionMajor = ($thisPSVersion).Major + $thisPSVersionMinor = ($thisPSVersion).Minor + $thisPSVersionPatch = ($thisPSVersion).Patch + + $psVersionCheckResult = 'letsCheck' + + if ($PSVersionTable.PSEdition -eq 'Core' -and $thisPSVersionMajor -eq $splitRequiredPSVersionMajor) { + if ($thisPSVersionMinor -gt $splitRequiredPSVersionMinor) { + $psVersionCheckResult = 'passed' + $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$thisPSVersionMinor] gt $($splitRequiredPSVersionMinor))" + } + else { + if ($thisPSVersionPatch -ge $splitRequiredPSVersionPatch) { + $psVersionCheckResult = 'passed' + $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$splitRequiredPSVersionMinor]; Patch[$thisPSVersionPatch] gt $($splitRequiredPSVersionPatch))" + } + else { + $psVersionCheckResult = 'failed' + $psVersionCheck = "(Major[$splitRequiredPSVersionMajor]; Minor[$splitRequiredPSVersionMinor]; Patch[$thisPSVersionPatch] lt $($splitRequiredPSVersionPatch))" + } + } + } + else { + $psVersionCheckResult = 'failed' + $psVersionCheck = "(Major[$splitRequiredPSVersionMajor] ne $($splitRequiredPSVersionMajor))" + } + + if ($psVersionCheckResult -eq 'passed') { + Write-Host " PS check $psVersionCheckResult : $($psVersionCheck); (minimum supported version '$requiredPSVersion')" + Write-Host " PS Edition: $($PSVersionTable.PSEdition); PS Version: $($PSVersionTable.PSVersion)" + Write-Host ' PS Version check succeeded' -ForegroundColor Green + } + else { + Write-Host " PS check $psVersionCheckResult : $($psVersionCheck)" + Write-Host " PS Edition: $($PSVersionTable.PSEdition); PS Version: $($PSVersionTable.PSVersion)" + Write-Host " Parallelization requires Powershell 'Core' version '$($requiredPSVersion)' or higher" + Throw 'Error - check the last console output for details' + } +} +function validateAccess { + #region validationAccess + #validation / check 'Microsoft Graph API' Access + $permissionCheckResults = @() + if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true -or $azAPICallConf['htParameters'].accountType -eq 'ServicePrincipal' -or $azAPICallConf['htParameters'].accountType -eq 'ManagedService' -or $azAPICallConf['htParameters'].accountType -eq 'ClientAssertion') { + + Write-Host "Checking $($azAPICallConf['htParameters'].accountType) permissions" + + $permissionsCheckFailed = $false + + $currentTask = 'Test MSGraph Users Read permission' + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/users?`$count=true&`$top=1" + $method = 'GET' + $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess + if ($res -eq 'failed') { + $permissionCheckResults += "MSGraph API 'Users Read' permission - check FAILED" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "MSGraph API 'Users Read' permission - check PASSED" + } + + $currentTask = 'Test MSGraph Groups Read permission' + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/groups?`$count=true&`$top=1" + $method = 'GET' + $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess + if ($res -eq 'failed') { + $permissionCheckResults += "MSGraph API 'Groups Read' permission - check FAILED" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "MSGraph API 'Groups Read' permission - check PASSED" + } + + $currentTask = 'Test MSGraph ServicePrincipals Read permission' + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/v1.0/servicePrincipals?`$count=true&`$top=1" + $method = 'GET' + $res = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -consistencyLevel 'eventual' -validateAccess + if ($res -eq 'failed') { + $permissionCheckResults += "MSGraph API 'ServicePrincipals Read' permission - check FAILED" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "MSGraph API 'ServicePrincipals Read' permission - check PASSED" + } + + if (-not $NoPIMEligibility) { + $currentTask = 'Test MSGraph PrivilegedAccess.Read.AzureResources permission' + $uriExt = "&`$expand=parent&`$filter=(type eq 'subscription' or type eq 'managementgroup')&`$top=1" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].MicrosoftGraph)/beta/privilegedAccess/azureResources/resources?`$select=id,displayName,type,externalId" + $uriExt + $res = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -currentTask $currentTask -validateAccess + if ($res -eq 'failed') { + $permissionCheckResults += "MSGraph API 'PrivilegedAccess.Read.AzureResources' permission - check FAILED - if you cannot grant this permission or you do not have an AAD Premium 2 license then use parameter -NoPIMEligibility" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "MSGraph API 'PrivilegedAccess.Read.AzureResources' permission - check PASSED" + } + } + } + #endregion validationAccess + + #ManagementGroup helper + #region managementGroupHelper + if (-not $ManagementGroupId) { + #$catchResult = "letscheck" + $currentTask = 'Getting all Management Groups' + #Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups?api-version=2020-05-01" + $method = 'GET' + $getAzManagementGroups = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -validateAccess + + if ($getAzManagementGroups -eq 'failed') { + $permissionCheckResults += "RBAC 'Reader' permissions on Management Group - check FAILED (use Id, not displayName)" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "RBAC 'Reader' permissions on Management Group - check PASSED" + } + + Write-Host 'Permission check results' + foreach ($permissionCheckResult in $permissionCheckResults) { + if ($permissionCheckResult -like '*PASSED*') { + Write-Host $permissionCheckResult -ForegroundColor Green + } + else { + Write-Host $permissionCheckResult -ForegroundColor DarkRed + } + } + if ($permissionsCheckFailed -eq $true) { + Write-Host "Please consult the documentation: https://$($GithubRepository)#required-permissions-in-azure" + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + + if ($getAzManagementGroups.Count -eq 0) { + Write-Host 'Management Groups count returned null' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + else { + Write-Host "Detected $($getAzManagementGroups.Count) Management Groups" + } + + [array]$MgtGroupArray = addIndexNumberToArray -array ($getAzManagementGroups) + if (-not $MgtGroupArray) { + Write-Host 'Seems you do not have access to any Management Group. Please make sure you have the required RBAC role [Reader] assigned on at least one Management Group' -ForegroundColor Red + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + + selectMg + + if ($($MgtGroupArray[$SelectedMG - 1].Name)) { + $script:ManagementGroupId = $($MgtGroupArray[$SelectedMG - 1].name) + $script:ManagementGroupName = $($MgtGroupArray[$SelectedMG - 1].properties.displayName) + } + else { + Write-Host 's.th. unexpected happened' -ForegroundColor Red + return + } + Write-Host "Selected Management Group: #$($SelectedMG) $ManagementGroupName (Id: $ManagementGroupId)" -ForegroundColor Green + Write-Host '_______________________________________' + } + else { + $currentTask = "Checking permissions for ManagementGroup '$ManagementGroupId'" + Write-Host $currentTask + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)?api-version=2020-05-01" + $method = 'GET' + $selectedManagementGroupId = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' -validateAccess + + if ($selectedManagementGroupId -eq 'failed') { + $permissionCheckResults += "RBAC 'Reader' permissions on Management Group '$($ManagementGroupId)' - check FAILED (use Id, not displayName)" + $permissionsCheckFailed = $true + } + else { + $permissionCheckResults += "RBAC 'Reader' permissions on Management Group '$($ManagementGroupId)' - check PASSED" + $script:ManagementGroupId = $selectedManagementGroupId.Name + $script:ManagementGroupName = $selectedManagementGroupId.properties.displayName + } + + Write-Host 'Permission check results' + foreach ($permissionCheckResult in $permissionCheckResults) { + if ($permissionCheckResult -like '*PASSED*') { + Write-Host $permissionCheckResult -ForegroundColor Green + } + else { + Write-Host $permissionCheckResult -ForegroundColor DarkRed + } + } + + if ($permissionsCheckFailed -eq $true) { + Write-Host "Please consult the documentation for permission requirements: https://$($GithubRepository)#technical-documentation" + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + } + #endregion managementGroupHelper + + if ($azAPICallConf['htParameters'].accountType -eq 'User') { + validateLeastPrivilegeForUser + } +} +function validateLeastPrivilegeForUser { + $currentTask = "Validate least priviledge (Azure Resource side) for executing user $($azapicallConf['htParameters'].userObjectId)" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$($azapicallConf['htParameters'].userObjectId)'" + $method = 'GET' + $getRoleAssignmentsForExecutingUserAtManagementGroupId = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri + $nonReaderRolesAssigned = ($getRoleAssignmentsForExecutingUserAtManagementGroupId.properties.RoleDefinitionId | Sort-object -Unique).where({$_ -notlike '*acdd72a7-3385-48ef-bd42-f606fba81ae7'}) + if ($nonReaderRolesAssigned.Count -gt 0) { + Write-Host "* * * LEAST PRIVILEGE ADVICE" -ForegroundColor DarkRed + Write-Host "The Azure Governance Visualizer script is executed with more permissions than required." + Write-Host "The executing identity '$($azapicallConf['checkContext'].Account.Id)' ($($azapicallConf['checkContext'].Account.Type)) Id: '$($azapicallConf['htparameters'].userObjectId)' has the following RBAC Role(s) assigned at Management Group scope '$ManagementGroupId':" + foreach ($nonReaderRoleAssigned in $nonReaderRolesAssigned) { + $currentTask = "Get RBAC Role definition '$nonReaderRoleAssigned'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($nonReaderRoleAssigned)?api-version=2022-04-01" + $method = 'GET' + $getRole = AzAPICall -AzAPICallConfiguration $azapicallConf -uri $uri -listenOn Content + + if ($getRole.properties.roleName -eq 'owner' -or $getRole.properties.roleName -eq 'contributor') { + Write-Host " - $($getRole.properties.roleName) ($($getRole.properties.type)) !!!" + } + else{ + Write-Host " - $($getRole.properties.roleName) ($($getRole.properties.type))" + } + } + Write-Host "The required Azure RBAC role at Management Group scope '$ManagementGroupId' is 'Reader' (acdd72a7-3385-48ef-bd42-f606fba81ae7)." + Write-Host "Recommendation: consider executing the script in context of a Service Principal with least privilege. Review the Azure Governance Visualizer Setup Guide at 'https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/blob/master/setup.md'" + Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor DarkRed + pause + } + else { + Write-Host "Azure Governance Visualizer Least Privilege check (Azure Resource side) for executing identity '$($azapicallConf['checkContext'].Account.Id)' ($($azapicallConf['checkContext'].Account.Type)) Id: '$($azapicallConf['htparameters'].userObjectId)' succeeded" -ForegroundColor Green + } +} +function verifyModules3rd { + [CmdletBinding()]Param( + [object]$modules + ) + + foreach ($module in $modules) { + $moduleVersion = $module.ModuleVersion + + if ($moduleVersion) { + Write-Host "Verify '$($module.ModuleName)' version '$moduleVersion'" + } + else { + Write-Host "Verify '$($module.ModuleName)' (latest)" + } + + $maxRetry = 3 + $tryCount = 0 + do { + $tryCount++ + if ($tryCount -gt $maxRetry) { + Write-Host " Managing '$($module.ModuleName)' failed (tried $($tryCount - 1)x)" + throw " Managing '$($module.ModuleName)' failed" + } + + $installModuleSuccess = $false + try { + if (-not $moduleVersion) { + Write-Host ' Check latest module version' + try { + $moduleVersion = (Find-Module -Name $($module.ModuleName)).Version + Write-Host " $($module.ModuleName) Latest module version: $moduleVersion" + } + catch { + Write-Host " $($module.ModuleName) - Check latest module version failed" + throw " $($module.ModuleName) - Check latest module version failed" + } + } + + if (-not $installModuleSuccess) { + try { + $moduleVersionLoaded = (Get-InstalledModule -Name $($module.ModuleName)).Version + if ([System.Version]$moduleVersionLoaded -eq [System.Version]$moduleVersion) { + $installModuleSuccess = $true + } + else { + Write-Host " $($module.ModuleName) - Deviating module version '$moduleVersionLoaded'" + if ([System.Version]$moduleVersionLoaded -gt [System.Version]$moduleVersion) { + if (($env:SYSTEM_TEAMPROJECTID -and $env:BUILD_REPOSITORY_ID) -or $env:GITHUB_ACTIONS) { + #AzDO or GH + throw " $($module.ModuleName) - Deviating module version $moduleVersionLoaded" + } + else { + Write-Host " Current module version '$moduleVersionLoaded' greater than the minimum required version '$moduleVersion' -> tolerated" -ForegroundColor Yellow + $installModuleSuccess = $true + } + } + else { + Write-Host " Current module version '$moduleVersionLoaded' lower than the minimum required version '$moduleVersion' -> failed" + throw " $($module.ModuleName) - Deviating module version $moduleVersionLoaded" + } + } + } + catch { + throw + } + } + } + catch { + Write-Host " '$($module.ModuleName) $moduleVersion' not installed" + if (($env:SYSTEM_TEAMPROJECTID -and $env:BUILD_REPOSITORY_ID) -or $env:GITHUB_ACTIONS) { + Write-Host " Installing $($module.ModuleName) module ($($moduleVersion))" + $installAzAPICallModuleTryCounter = 0 + do { + $installAzAPICallModuleTryCounter++ + try { + $params = @{ + Name = "$($module.ModuleName)" + Force = $true + RequiredVersion = $moduleVersion + ErrorAction = 'Stop' + } + Install-Module @params + $installAzAPICallModuleSuccess = $true + Write-Host " Try#$($installAzAPICallModuleTryCounter) Installing '$($module.ModuleName)' module ($($moduleVersion)) succeeded" + } + catch { + Write-Host " Try#$($installAzAPICallModuleTryCounter) Installing '$($module.ModuleName)' module ($($moduleVersion)) failed - sleep $($installAzAPICallModuleTryCounter) seconds" + Start-Sleep -Seconds $installAzAPICallModuleTryCounter + $installAzAPICallModuleSuccess = $false + } + } + until($installAzAPICallModuleTryCounter -gt 10 -or $installAzAPICallModuleSuccess) + if (-not $installAzAPICallModuleSuccess) { + throw " Installing '$($module.ModuleName)' module ($($moduleVersion)) failed" + } + + } + else { + do { + $installModuleUserChoice = $null + $installModuleUserChoice = Read-Host " Do you want to install $($module.ModuleName) module ($($moduleVersion)) from the PowerShell Gallery? (y/n)" + if ($installModuleUserChoice -eq 'y') { + try { + Install-Module -Name $module.ModuleName -RequiredVersion $moduleVersion -Force -ErrorAction Stop + try { + Import-Module -Name $module.ModuleName -RequiredVersion $moduleVersion -Force -ErrorAction Stop + } + catch { + throw " 'Import-Module -Name $($module.ModuleName) -RequiredVersion $moduleVersion -Force' failed" + } + } + catch { + throw " 'Install-Module -Name $($module.ModuleName) -RequiredVersion $moduleVersion' failed" + } + } + elseif ($installModuleUserChoice -eq 'n') { + Write-Host " $($module.ModuleName) module is required, please visit https://aka.ms/$($module.ModuleProductName) or https://www.powershellgallery.com/packages/$($module.ModuleProductName)" + throw " $($module.ModuleName) module is required" + } + else { + Write-Host " Accepted input 'y' or 'n'; start over.." + } + } + until ($installModuleUserChoice -eq 'y') + } + } + } + until ($installModuleSuccess) + Write-Host " Verify '$($module.ModuleName)' version '$moduleVersion' succeeded" -ForegroundColor Green + } +} +#region functions4DataCollection + +function dataCollectionMGSecureScore { + [CmdletBinding()]Param( + [string]$Id + ) + + $mgAscSecureScoreResult = '' + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { + if ($htMgASCSecureScore.($Id)) { + $mgAscSecureScoreResult = $htMgASCSecureScore.($Id).SecureScore + } + else { + $mgAscSecureScoreResult = 'isNullOrEmpty' + } + } + return $mgAscSecureScoreResult +} +$funcDataCollectionMGSecureScore = $function:dataCollectionMGSecureScore.ToString() + +function dataCollectionDefenderPlans { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $SubscriptionQuotaId + ) + + $currentTask = "Getting Microsoft Defender for Cloud plans for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/pricings?api-version=2018-06-01" + $method = 'GET' + $defenderPlansResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($defenderPlansResult -eq 'SubScriptionNotRegistered' -or $defenderPlansResult -eq 'DisallowedProvider') { + #Subscription skipped for MDfC + $null = $script:arrayDefenderPlansSubscriptionsSkipped.Add([PSCustomObject]@{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + subscriptionQuotaId = $subscriptionQuotaId + subscriptionMgPath = $childMgMgPath + reason = $defenderPlansResult + }) + } + else { + if ($defenderPlansResult.Count -gt 0) { + foreach ($defenderPlanResult in $defenderPlansResult) { + $null = $script:arrayDefenderPlans.Add([PSCustomObject]@{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + subscriptionMgPath = $childMgMgPath + defenderPlan = $defenderPlanResult.name + defenderPlanTier = $defenderPlanResult.properties.pricingTier + }) + } + } + } +} +$funcDataCollectionDefenderPlans = $function:dataCollectionDefenderPlans.ToString() + + +function dataCollectionAdvisorScores { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $SubscriptionQuotaId + ) + + $currentTask = "Getting Advisor Scores for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Advisor/advisorScore?api-version=2020-07-01-preview" + $method = 'GET' + $advisorScoreResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -skipOnErrorCode 404 + + if ($advisorScoreResult -eq 'SubScriptionNotRegistered' -or $advisorScoreResult -eq 'DisallowedProvider') { + } + else { + if ($advisorScoreResult -like 'azgvzerrorMessage_*') { + + } + else { + if ($advisorScoreResult.Count -gt 0) { + foreach ($entry in $advisorScoreResult) { + #Write-Host ($entry | ConvertTo-Json -Depth 99) + if ($entry.Name) { + $objectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($entry.Name, [System.Management.Automation.PSReference]$ObjectGuid)) { + } + else { + $null = $script:arrayAdvisorScores.Add([PSCustomObject]@{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + subscriptionQuotaId = $SubscriptionQuotaId + subscriptionMgPath = $childMgMgPath + category = $entry.Name + score = $entry.properties.lastRefreshedScore.score + }) + } + } + } + } + } + } +} +$funcDataCollectionAdvisorScores = $function:dataCollectionAdvisorScores.ToString() + +function dataCollectionDefenderEmailContacts { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $SubscriptionQuotaId + ) + + $currentTask = "Getting Microsoft Defender for Cloud Email contacts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" + $method = 'GET' + $defenderSecurityContactsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -listenOn 'Content' -currentTask $currentTask -caller 'CustomDataCollection' + + if ($defenderSecurityContactsResult -eq 'SubScriptionNotRegistered' -or $defenderSecurityContactsResult -eq 'DisallowedProvider') { + } + else { + + if ($defenderSecurityContactsResult -like 'azgvzerrorMessage_*') { + $errorInfo = $defenderSecurityContactsResult -replace 'azgvzerrorMessage_' + $script:htDefenderEmailContacts.($scopeId) = @{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + emails = $errorInfo + roles = $errorInfo + alertNotificationsState = $errorInfo + alertNotificationsminimalSeverity = $errorInfo + } + } + else { + if ($defenderSecurityContactsResult.Count -gt 0) { + foreach ($entry in $defenderSecurityContactsResult) { + + if ($entry.properties) { + if ($entry.properties.notificationsByRole.roles.count -gt 0) { + $roles = ($entry.properties.notificationsByRole.roles | Sort-Object) -join "$CsvDelimiterOpposite " + } + else { + $roles = 'none' + } + + if ($entry.properties.emails) { + if (-not [string]::IsNullOrWhiteSpace($entry.properties.emails)) { + $emailsSplitted = $entry.properties.emails -split ';' + $arrayEmails = @() + foreach ($email in $emailsSplitted) { + $arrayEmails += "'$email'" + } + $emails = ($arrayEmails | Sort-Object) -join "$CsvDelimiterOpposite " + } + else { + $emails = $entry.properties.emails + } + } + else { + $emails = 'none' + } + + if ($entry.properties.alertNotifications.state) { + $alertNotificationsState = $entry.properties.alertNotifications.state + } + + if ($entry.properties.alertNotifications.minimalSeverity) { + $alertNotificationsminimalSeverity = $entry.properties.alertNotifications.minimalSeverity + } + } + else { + $roles = 'n/a' + $emails = 'n/a' + $alertNotificationsState = 'n/a' + $alertNotificationsminimalSeverity = 'n/a' + } + + $script:htDefenderEmailContacts.($scopeId) = @{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + emails = $emails + roles = $roles + alertNotificationsState = $alertNotificationsState + alertNotificationsminimalSeverity = $alertNotificationsminimalSeverity + } + } + } + else { + $script:htDefenderEmailContacts.($scopeId) = @{ + subscriptionId = $scopeId + subscriptionName = $scopeDisplayName + emails = 'n/a' + roles = 'n/a' + alertNotificationsState = 'n/a' + alertNotificationsminimalSeverity = 'n/a' + } + } + } + } +} +$funcDataCollectionDefenderEmailContacts = $function:dataCollectionDefenderEmailContacts.ToString() + +function dataCollectionVNets { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $SubscriptionQuotaId + ) + + $currentTask = "Getting Virtual Networks for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/virtualNetworks?api-version=2022-05-01" + $method = 'GET' + $networkResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($networkResult -eq 'someError') { + } + else { + if ($networkResult.Count -gt 0) { + if ($networkResult -ne 'DisallowedProvider') { + foreach ($vnet in $networkResult) { + $null = $script:arrayVNets.Add($vnet) + } + } + } + } +} +$funcDataCollectionVNets = $function:dataCollectionVNets.ToString() + +function dataCollectionPrivateEndpoints { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $SubscriptionQuotaId + ) + + $currentTask = "Getting Private Endpoints for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$SubscriptionQuotaId']" + #https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Network/privateEndpoints?api-version=2022-05-01" + $method = 'GET' + $privateEndpointsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue + + if ($privateEndpointsResult.Count -gt 0) { + if ($privateEndpointsResult -ne 'DisallowedProvider') { + foreach ($pe in $privateEndpointsResult) { + $null = $script:arrayPrivateEndPoints.Add($pe) + } + } + } +} +$funcDataCollectionPrivateEndpoints = $function:dataCollectionPrivateEndpoints.ToString() + +function dataCollectionDiagnosticsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $ChildMgId, + $subscriptionQuotaId + ) + + $currentTask = "Getting Diagnostic Settings for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview" + $method = 'GET' + $getDiagnosticSettingsSub = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($getDiagnosticSettingsSub.Count -eq 0) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Sub' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $childMgMgPath + SubMgParent = $childMgId + DiagnosticsPresent = 'false' + }) + } + else { + foreach ($diagnosticSetting in $getDiagnosticSettingsSub) { + $arrayLogs = [System.Collections.ArrayList]@() + if ($diagnosticSetting.Properties.logs) { + foreach ($logCategory in $diagnosticSetting.properties.logs) { + $null = $arrayLogs.Add([PSCustomObject]@{ + Category = $logCategory.category + Enabled = $logCategory.enabled + }) + } + } + + $htLogs = @{} + if ($diagnosticSetting.Properties.logs) { + foreach ($logCategory in $diagnosticSetting.properties.logs) { + if ($logCategory.enabled) { + $htLogs.($logCategory.category) = 'true' + } + else { + $htLogs.($logCategory.category) = 'false' + } + } + } + + if ($diagnosticSetting.Properties.workspaceId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Sub' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $childMgMgPath + SubMgParent = $childMgId + DiagnosticsPresent = 'true' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'LA' + DiagnosticTargetId = $diagnosticSetting.Properties.workspaceId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + if ($diagnosticSetting.Properties.storageAccountId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Sub' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $childMgMgPath + SubMgParent = $childMgId + DiagnosticsPresent = 'true' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'SA' + DiagnosticTargetId = $diagnosticSetting.Properties.storageAccountId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + if ($diagnosticSetting.Properties.eventHubAuthorizationRuleId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Sub' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $childMgMgPath + SubMgParent = $childMgId + DiagnosticsPresent = 'true' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'EH' + DiagnosticTargetId = $diagnosticSetting.Properties.eventHubAuthorizationRuleId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + } + } +} +$funcDataCollectionDiagnosticsSub = $function:dataCollectionDiagnosticsSub.ToString() + +function dataCollectionDiagnosticsMG { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName + ) + + $mgPath = $htManagementGroupsMgPath.($scopeId).pathDelimited + $currentTask = "Getting Diagnostic Settings for Management Group: '$($scopeDisplayName)' ('$($scopeId)')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($mgdetail.Name)/providers/microsoft.insights/diagnosticSettings?api-version=2020-01-01-preview" + $method = 'GET' + $getDiagnosticSettingsMg = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask + + if ($getDiagnosticSettingsMg -eq 'InvalidResourceType') { + #skipping until supported + } + else { + if ($getDiagnosticSettingsMg.Count -eq 0) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Mg' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $mgPath + DiagnosticsPresent = 'false' + DiagnosticsInheritedOrnot = $false + DiagnosticsInheritedFrom = 'none' + }) + } + else { + foreach ($diagnosticSetting in $getDiagnosticSettingsMg) { + $arrayLogs = [System.Collections.ArrayList]@() + if ($diagnosticSetting.Properties.logs) { + foreach ($logCategory in $diagnosticSetting.properties.logs) { + $null = $arrayLogs.Add([PSCustomObject]@{ + Category = $logCategory.category + Enabled = $logCategory.enabled + }) + } + } + + $htLogs = @{} + if ($diagnosticSetting.Properties.logs) { + foreach ($logCategory in $diagnosticSetting.properties.logs) { + if ($logCategory.enabled) { + $htLogs.($logCategory.category) = 'true' + } + else { + $htLogs.($logCategory.category) = 'false' + } + } + } + + if ($diagnosticSetting.Properties.workspaceId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Mg' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $mgPath + DiagnosticsPresent = 'true' + DiagnosticsInheritedOrnot = $false + DiagnosticsInheritedFrom = 'none' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'LA' + DiagnosticTargetId = $diagnosticSetting.Properties.workspaceId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + if ($diagnosticSetting.Properties.storageAccountId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Mg' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $mgPath + DiagnosticsPresent = 'true' + DiagnosticsInheritedOrnot = $false + DiagnosticsInheritedFrom = 'none' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'SA' + DiagnosticTargetId = $diagnosticSetting.Properties.storageAccountId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + if ($diagnosticSetting.Properties.eventHubAuthorizationRuleId) { + $null = $script:arrayDiagnosticSettingsMgSub.Add([PSCustomObject]@{ + Scope = 'Mg' + ScopeName = $scopeDisplayName + ScopeId = $scopeId + ScopeMgPath = $mgPath + DiagnosticsPresent = 'true' + DiagnosticsInheritedOrnot = $false + DiagnosticsInheritedFrom = 'none' + DiagnosticSettingName = $diagnosticSetting.name + DiagnosticTargetType = 'EH' + DiagnosticTargetId = $diagnosticSetting.Properties.eventHubAuthorizationRuleId + DiagnosticCategories = $arrayLogs + DiagnosticCategoriesHt = $htLogs + }) + } + } + } + } +} +$funcDataCollectionDiagnosticsMG = $function:dataCollectionDiagnosticsMG.ToString() + +function dataCollectionStorageAccounts { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $ChildMgParentNameChainDelimited, + $subscriptionQuotaId + ) + + $currentTask = "Getting Storage Accounts for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Storage/storageAccounts?api-version=2021-09-01" + $method = 'GET' + $storageAccountsSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($storageAccountsSubscriptionResult -ne 'DisallowedProvider') { + foreach ($storageAccount in $storageAccountsSubscriptionResult) { + + $dtisostart = Get-Date (Get-Date).AddHours(-1).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' + $dtisoend = Get-Date (Get-Date).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' + $currentTask = "Getting Storage Account '$($storageAccount.name)' UsedCapacity ('$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId'])" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($storageAccount.id)/providers/Microsoft.Insights/metrics?timespan=$($dtisostart)/$($dtisoend)&metricnames=UsedCapacity&aggregation=Average&api-version=2021-05-01" + $method = 'GET' + $storageAccountUsedCapacity = $null + $storageAccountUsedCapacity = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -unhandledErrorAction Continue + + $usedCapacity = 'n/a' + if ($storageAccountUsedCapacity.Count -gt 0) { + if (-not [string]::IsNullOrWhiteSpace($storageAccountUsedCapacity.timeseries.data.average)) { + $usedCapacity = [decimal]$storageAccountUsedCapacity.timeseries.data.average / 1024 / 1024 / 1024 + } + } + + $obj = [System.Collections.ArrayList]@() + $null = $obj.Add([PSCustomObject]@{ + SA = $storageAccount + SAUsedCapacity = $usedCapacity + }) + $null = $script:storageAccounts.Add($obj) + } + } + +} +$funcDataCollectionStorageAccounts = $function:dataCollectionStorageAccounts.ToString() + +function dataCollectionResources { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $ChildMgMgPath, + $ChildMgParentNameChainDelimited, + $subscriptionQuotaId + ) + + #region resources LIST + $currentTask = "Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resources?`$expand=createdTime,changedTime,properties&api-version=2023-07-01" + $method = 'GET' + $resourcesSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + #Write-Host 'arm resList count:'$resourcesSubscriptionResult.Count + #endregion resources LIST + + #region resources GET + if ($resourcesSubscriptionResult.Count -gt 0) { + $arrayResourcesWithProperties = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $resourcesSubscriptionResult | ForEach-Object -Parallel { + $resource = $_ + + #region using + $arrayResourcesWithProperties = $using:arrayResourcesWithProperties + $htResourceProvidersRef = $using:htResourceProvidersRef + $arrayPrivateEndPointsFromResourceProperties = $using:arrayPrivateEndPointsFromResourceProperties + $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes + $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed + $scopeId = $using:scopeId + $scopeDisplayName = $using:scopeDisplayName + $ChildMgParentNameChainDelimited = $using:ChildMgParentNameChainDelimited + $azAPICallConf = $using:azAPICallConf + #$htResourcesWithProperties = $using:htResourcesWithProperties + #endregion using + + if ($htAvailablePrivateEndpointTypes.(($resource.type).ToLower())) { + #Write-Host "$($resource.type) in `$htAvailablePrivateEndpointTypes" + if ($htResourceProvidersRef.($resource.type)) { + if ($htResourceProvidersRef.($resource.type).APIDefault) { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APIDefault + $apiRef = 'default' + } + else { + $apiVersionToUse = $htResourceProvidersRef.($resource.type).APILatest + $apiRef = 'latest' + } + + $currentTask = "Getting Resource Properties API-version: '$apiVersionToUse' ($apiRef); ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)'" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)$($resource.id)?api-version=$apiVersionToUse" + $method = 'GET' + $resourceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' -listenOn Content -unhandledErrorAction Continue + + if ($resourceResult -ne 'ResourceOrResourcegroupNotFound' -and $resourceResult -ne 'convertfromJSONError') { + $null = $script:arrayResourcesWithProperties.Add($resourceResult) + #$script:htResourcesWithProperties.($resourceResult.id) = $resourceResult + if ($resourceResult.properties.privateEndpointConnections.Count -gt 0) { + foreach ($privateEndpointConnection in $resourceResult.properties.privateEndpointConnections) { + $resourceResultIdSplit = $resourceResult.id -split '/' + $null = $script:arrayPrivateEndPointsFromResourceProperties.Add([PSCustomObject]@{ + ResourceName = $resourceResult.name + ResourceType = $resourceResult.type + ResourceId = $resourceResult.id + ResourceResourceGroup = $resourceResultIdSplit[4] + ResourceSubscriptionId = $scopeId + ResourceSubscriptionName = $scopeDisplayName + ResourceMGPath = $ChildMgParentNameChainDelimited + privateEndpointConnection = $privateEndpointConnection + }) + } + } + } + else { + if ($resourceResult -eq 'convertfromJSONError') { + $script:htResourcePropertiesConvertfromJSONFailed.($resource.id) = @{} + } + } + } + else { + Write-Host "[Azure Governance Visualizer] Please file an issue at the Azure Governance Visualizer GitHub repository (aka.ms/AzGovViz) and provide this information (scrub subscription Id and company identifyable names): No API-version matches! ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)' - Thank you!" -ForegroundColor DarkRed + } + } + else { + #Write-Host "$($resource.type) not in `$htAvailablePrivateEndpointTypes" + } + + } -ThrottleLimit $azAPICallConf['htParameters'].ThrottleLimit + } + #Write-Host 'arm resGet count:' $arrayResourcesWithProperties.Count + #endregion resources GET + + # if ($resourcesSubscriptionResult.Count -ne $arrayResourcesWithProperties.Count) { + # Write-Host " FYI: Getting Resources for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId'] - ARM list count: $($resourcesSubscriptionResult.Count); ARG get count: $($arrayResourcesWithProperties.Count)" + # } + + #region PSRule + if ($azAPICallConf['htParameters'].DoPSRule -eq $true) { + if ($resourcesSubscriptionResult.Count -gt 0) { + + $startPSRule = Get-Date + try { + <# + $path = (Get-Module PSRule.Rules.Azure -ListAvailable | Sort-Object Version -Descending -Top 1).ModuleBase + Write-Host "Import-Module (Join-Path $path -ChildPath 'PSRule.Rules.Azure-nodeps.psd1')" + Import-Module (Join-Path $path -ChildPath 'PSRule.Rules.Azure-nodeps.psd1') + #> + if ($azAPICallConf['htParameters'].PSRuleFailedOnly -eq $true) { + $psruleResults = $arrayResourcesWithProperties | Invoke-PSRule -Module psrule.rules.Azure -As Detail -Culture en-us -WarningAction Ignore -ErrorAction SilentlyContinue -Outcome Fail, Error + } + else { + $psruleResults = $arrayResourcesWithProperties | Invoke-PSRule -Module psrule.rules.Azure -As Detail -Culture en-us -WarningAction Ignore -ErrorAction SilentlyContinue + } + } + catch { + Write-Host " Please report 'PSRule for Azure' error '$($scopeDisplayName)' ('$scopeId'): $_" + } + + $endPSRule = Get-Date + $durationPSRule = $((New-TimeSpan -Start $startPSRule -End $endPSRule).TotalSeconds) + + $null = $script:arrayPSRuleTracking.Add([PSCustomObject]@{ + subscriptionId = $scopeId + duration = $durationPSRule + }) + + if ($psruleResults.Count -gt 0) { + foreach ($psRuleResult in $psRuleResults) { + $null = $script:arrayPSRule.Add([PSCustomObject]@{ + resourceType = $psRuleResult.TargetType + subscriptionId = $scopeId + mgPath = $ChildMgParentNameChainDelimited + resourceId = $psRuleResult.TargetObject.id + pillar = $psRuleResult.Info.Annotations.pillar + category = $psRuleResult.Info.Annotations.category + severity = $psRuleResult.Info.Annotations.severity + rule = $psRuleResult.Info.DisplayName + description = $psRuleResult.Info.Description + recommendation = $psRuleResult.Info.Recommendation + link = $psRuleResult.Info.Annotations.'online version' + result = $psRuleResult.Outcome + errorMsg = $psRuleResult.Error.Message + }) + } + } + } + } + #endregion PSRule + + foreach ($resourceTypeLocation in ($resourcesSubscriptionResult | Group-Object -Property type, location)) { + $null = $script:resourcesAll.Add([PSCustomObject]@{ + subscriptionId = $scopeId + type = ($resourceTypeLocation.values[0]).ToLower() + location = ($resourceTypeLocation.values[1]).ToLower() + count_ = $resourceTypeLocation.Count + }) + } + + foreach ($resourceType in ($resourcesSubscriptionResult | Group-Object -Property type)) { + if (-not $htResourceTypesUniqueResource.(($resourceType.name).ToLower())) { + $script:htResourceTypesUniqueResource.(($resourceType.name).ToLower()) = @{} + $script:htResourceTypesUniqueResource.(($resourceType.name).ToLower()).resourceId = $resourceType.Group.Id | Select-Object -First 1 + } + } + + $startSubResourceIdsThis = Get-Date + + <# Build the $JSONcafResourceNaming #pending PR https://github.com/MicrosoftDocs/cloud-adoption-framework/pull/916 + $arrayCAFNamingConvention = [System.Collections.ArrayList]@() + $htCAFNamingConvention = @{} + #$cafNamingFromFile = Get-Content -Path .\cafNaming.md -Encoding utf8 + $CAFFileName = 'resource-abbreviations.md' + Invoke-webrequest -OutFile .\$($CAFFileName) -URI "https://raw.githubusercontent.com/MicrosoftDocs/cloud-adoption-framework/main/docs/ready/azure-best-practices/resource-abbreviations.md" + $cafNamingFromFile = Get-Content -Path .\$($CAFFileName) -Encoding utf8 + $cafNamingFromFile.count + foreach ($line in $cafNamingFromFile) { + #$line + if ($line -match "microsoft.") { + $tranformed = $line -replace '`' -split " \| " + $friendlyName = $($tranformed[0] -replace "\| ") + $resourceType = $($tranformed[1]) + $namingConvention = $($tranformed[2] -replace " \|" -replace "\|") + $null = $arrayCAFNamingConvention.Add([PSCustomObject]@{ + resourceType = $resourceType + friendlyName = $friendlyName + namingConvention = $namingConvention + }) + } + } + + $htCAFNamingConvention = [ordered]@{} + $arrayCAFNamingConventionGroupedByType = $arrayCAFNamingConvention | Sort-Object -Property resourceType | Group-Object -Property resourceType + foreach ($entry in $arrayCAFNamingConventionGroupedByType){ + $htCAFNamingConvention.($entry.name) = @{} + $htCAFNamingConvention.($entry.name).friendlyName = $entry.group.friendlyName + $htCAFNamingConvention.($entry.name).namingConvention = $entry.group.namingConvention + } + $htCAFNamingConvention | ConvertTo-Json +#> + + $JSONcafResourceNaming = @' + { + "Microsoft.AnalysisServices/servers": { + "friendlyName": "Azure Analysis Services server", + "namingConvention": "as" + }, + "Microsoft.ApiManagement/service": { + "friendlyName": "API management service instance", + "namingConvention": "apim-" + }, + "Microsoft.AppConfiguration/configurationStores": { + "friendlyName": "App Configuration store", + "namingConvention": "appcs-" + }, + "Microsoft.Authorization/policyDefinitions": { + "friendlyName": "Policy definition", + "namingConvention": "policy-" + }, + "Microsoft.Automation/automationAccounts": { + "friendlyName": "Automation account", + "namingConvention": "aa-" + }, + "Microsoft.Blueprint/blueprints": { + "friendlyName": "Blueprint", + "namingConvention": "bp-" + }, + "Microsoft.Blueprint/blueprints/artifacts": { + "friendlyName": "Blueprint assignment", + "namingConvention": "bpa-" + }, + "Microsoft.Cache/Redis": { + "friendlyName": "Azure Cache for Redis instance", + "namingConvention": "redis-" + }, + "Microsoft.Cdn/profiles": { + "friendlyName": "CDN profile", + "namingConvention": "cdnp-" + }, + "Microsoft.Cdn/profiles/endpoints": { + "friendlyName": "CDN endpoint", + "namingConvention": "cdne-" + }, + "Microsoft.CognitiveServices/accounts": { + "friendlyName": "Azure Cognitive Services", + "namingConvention": "cog-" + }, + "Microsoft.Compute/availabilitySets": { + "friendlyName": "Availability set", + "namingConvention": "avail-" + }, + "Microsoft.Compute/cloudServices": { + "friendlyName": "Cloud service", + "namingConvention": "cld-" + }, + "Microsoft.Compute/diskEncryptionSets": { + "friendlyName": "Disk encryption set", + "namingConvention": "des" + }, + "Microsoft.Compute/disks": { + "friendlyName": [ + "Managed disk (data)", + "Managed disk (OS)" + ], + "namingConvention": [ + "disk", + "osdisk" + ] + }, + "Microsoft.Compute/galleries": { + "friendlyName": "Gallery", + "namingConvention": "gal" + }, + "Microsoft.Compute/snapshots": { + "friendlyName": "Snapshot", + "namingConvention": "snap-" + }, + "Microsoft.Compute/virtualMachines": { + "friendlyName": "Virtual machine", + "namingConvention": "vm" + }, + "Microsoft.Compute/virtualMachineScaleSets": { + "friendlyName": "Virtual machine scale set", + "namingConvention": "vmss-" + }, + "Microsoft.ContainerInstance/containerGroups": { + "friendlyName": "Container instance", + "namingConvention": "ci" + }, + "Microsoft.ContainerRegistry/registries": { + "friendlyName": "Container registry", + "namingConvention": "cr" + }, + "Microsoft.ContainerService/managedClusters": { + "friendlyName": "AKS cluster", + "namingConvention": "aks-" + }, + "Microsoft.Databricks/workspaces": { + "friendlyName": "Azure Databricks workspace", + "namingConvention": "dbw-" + }, + "Microsoft.DataFactory/factories": { + "friendlyName": "Azure Data Factory", + "namingConvention": "adf-" + }, + "Microsoft.DataLakeAnalytics/accounts": { + "friendlyName": "Data Lake Analytics account", + "namingConvention": "dla" + }, + "Microsoft.DataLakeStore/accounts": { + "friendlyName": "Data Lake Store account", + "namingConvention": "dls" + }, + "Microsoft.DataMigration/services": { + "friendlyName": "Database Migration Service instance", + "namingConvention": "dms-" + }, + "Microsoft.DataProtection/BackupVaults": { + "friendlyName": "Backup vault", + "namingConvention": "bv-" + }, + "Microsoft.DBforMySQL/servers": { + "friendlyName": "MySQL database", + "namingConvention": "mysql-" + }, + "Microsoft.DBforPostgreSQL/servers": { + "friendlyName": "PostgreSQL database", + "namingConvention": "psql-" + }, + "Microsoft.Devices/IotHubs": { + "friendlyName": "IoT hub", + "namingConvention": "iot-" + }, + "Microsoft.Devices/provisioningServices": { + "friendlyName": "Provisioning services", + "namingConvention": "provs-" + }, + "Microsoft.Devices/provisioningServices/certificates": { + "friendlyName": "Provisioning services certificate", + "namingConvention": "pcert-" + }, + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases": { + "friendlyName": "Azure Cosmos DB database", + "namingConvention": "cosmos-" + }, + "Microsoft.EventGrid/domains": { + "friendlyName": "Event Grid domain", + "namingConvention": "evgd-" + }, + "Microsoft.EventGrid/domains/topics": { + "friendlyName": "Event Grid topic", + "namingConvention": "evgt-" + }, + "Microsoft.EventGrid/eventSubscriptions": { + "friendlyName": "Event Grid subscriptions", + "namingConvention": "evgs-" + }, + "Microsoft.EventHub/namespaces": { + "friendlyName": "Event Hubs namespace", + "namingConvention": "evhns-" + }, + "Microsoft.EventHub/namespaces/eventHubs": { + "friendlyName": "Event hub", + "namingConvention": "evh-" + }, + "Microsoft.HDInsight/clusters": { + "friendlyName": [ + "HDInsight - Hadoop cluster", + "HDInsight - Kafka cluster", + "HDInsight - Spark cluster", + "HDInsight - Storm cluster", + "HDInsight - ML Services cluster", + "HDInsight - HBase cluster" + ], + "namingConvention": [ + "hadoop-", + "kafka-", + "spark-", + "storm-", + "mls-", + "hbase-" + ] + }, + "Microsoft.HybridCompute/machines": { + "friendlyName": "Azure Arc enabled server", + "namingConvention": "arcs-" + }, + "Microsoft.Insights/actionGroups": { + "friendlyName": "Azure Monitor action group", + "namingConvention": "ag-" + }, + "Microsoft.Insights/components": { + "friendlyName": "Application Insights", + "namingConvention": "appi-" + }, + "Microsoft.KeyVault/vaults": { + "friendlyName": "Key vault", + "namingConvention": "kv-" + }, + "Microsoft.Kubernetes/connectedClusters": { + "friendlyName": "Azure Arc enabled Kubernetes cluster", + "namingConvention": "arck" + }, + "Microsoft.Kusto/clusters": { + "friendlyName": "Azure Data Explorer cluster", + "namingConvention": "dec" + }, + "Microsoft.Kusto/clusters/databases": { + "friendlyName": "Azure Data Explorer cluster database", + "namingConvention": "dedb" + }, + "Microsoft.Logic/integrationAccounts": { + "friendlyName": "Integration account", + "namingConvention": "ia-" + }, + "Microsoft.Logic/workflows": { + "friendlyName": "Logic apps", + "namingConvention": "logic-" + }, + "Microsoft.MachineLearningServices/workspaces": { + "friendlyName": "Azure Machine Learning workspace", + "namingConvention": "mlw-" + }, + "Microsoft.ManagedIdentity/userAssignedIdentities": { + "friendlyName": "Managed Identity", + "namingConvention": "id-" + }, + "Microsoft.Management/managementGroups": { + "friendlyName": "Management group", + "namingConvention": "mg-" + }, + "Microsoft.Migrate/assessmentProjects": { + "friendlyName": "Azure Migrate project", + "namingConvention": "migr-" + }, + "Microsoft.Network/applicationGateways": { + "friendlyName": "Application gateway", + "namingConvention": "agw-" + }, + "Microsoft.Network/applicationSecurityGroups": { + "friendlyName": "Application security group (ASG)", + "namingConvention": "asg-" + }, + "Microsoft.Network/azureFirewalls": { + "friendlyName": "Firewall", + "namingConvention": "afw-" + }, + "Microsoft.Network/bastionHosts": { + "friendlyName": "Bastion", + "namingConvention": "bas-" + }, + "Microsoft.Network/connections": { + "friendlyName": "Connections", + "namingConvention": "con-" + }, + "Microsoft.Network/dnsZones": { + "friendlyName": "DNS", + "namingConvention": "dnsz-" + }, + "Microsoft.Network/expressRouteCircuits": { + "friendlyName": "ExpressRoute circuit", + "namingConvention": "erc-" + }, + "Microsoft.Network/firewallPolicies": { + "friendlyName": [ + "Web Application Firewall (WAF) policy", + "Firewall policy" + ], + "namingConvention": [ + "waf", + "afwp-" + ] + }, + "Microsoft.Network/firewallPolicies/ruleGroups": { + "friendlyName": "Web Application Firewall (WAF) policy rule group", + "namingConvention": "wafrg" + }, + "Microsoft.Network/frontDoors": { + "friendlyName": "Front Door instance", + "namingConvention": "fd-" + }, + "Microsoft.Network/frontdoorWebApplicationFirewallPolicies": { + "friendlyName": "Front Door firewall policy", + "namingConvention": "fdfp-" + }, + "Microsoft.Network/loadBalancers": { + "friendlyName": [ + "Load balancer (external)", + "Load balancer (internal)" + ], + "namingConvention": [ + "lbe-", + "lbi-" + ] + }, + "Microsoft.Network/loadBalancers/inboundNatRules": { + "friendlyName": "Load balancer rule", + "namingConvention": "rule-" + }, + "Microsoft.Network/localNetworkGateways": { + "friendlyName": "Local network gateway", + "namingConvention": "lgw-" + }, + "Microsoft.Network/natGateways": { + "friendlyName": "NAT gateway", + "namingConvention": "ng-" + }, + "Microsoft.Network/networkInterfaces": { + "friendlyName": "Network interface (NIC)", + "namingConvention": "nic-" + }, + "Microsoft.Network/networkSecurityGroups": { + "friendlyName": "Network security group (NSG)", + "namingConvention": "nsg-" + }, + "Microsoft.Network/networkSecurityGroups/securityRules": { + "friendlyName": "Network security group (NSG) security rules", + "namingConvention": "nsgsr-" + }, + "Microsoft.Network/networkWatchers": { + "friendlyName": "Network Watcher", + "namingConvention": "nw-" + }, + "Microsoft.Network/privateDnsZones": { + "friendlyName": "DNS zone", + "namingConvention": "pdnsz-" + }, + "Microsoft.Network/privateLinkServices": { + "friendlyName": "Private Link", + "namingConvention": "pl-" + }, + "Microsoft.Network/publicIPAddresses": { + "friendlyName": "Public IP address", + "namingConvention": "pip-" + }, + "Microsoft.Network/publicIPPrefixes": { + "friendlyName": "Public IP address prefix", + "namingConvention": "ippre-" + }, + "Microsoft.Network/routeFilters": { + "friendlyName": "Route filter", + "namingConvention": "rf-" + }, + "Microsoft.Network/routeTables": { + "friendlyName": "Route table", + "namingConvention": "rt-" + }, + "Microsoft.Network/routeTables/routes": { + "friendlyName": "User defined route (UDR)", + "namingConvention": "udr-" + }, + "Microsoft.Network/trafficManagerProfiles": { + "friendlyName": "Traffic Manager profile", + "namingConvention": "traf-" + }, + "Microsoft.Network/virtualNetworkGateways": { + "friendlyName": "Virtual network gateway", + "namingConvention": "vgw-" + }, + "Microsoft.Network/virtualNetworks": { + "friendlyName": "Virtual network", + "namingConvention": "vnet-" + }, + "Microsoft.Network/virtualNetworks/subnets": { + "friendlyName": "Virtual network subnet", + "namingConvention": "snet-" + }, + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings": { + "friendlyName": "Virtual network peering", + "namingConvention": "peer-" + }, + "Microsoft.Network/virtualWans": { + "friendlyName": "Virtual WAN", + "namingConvention": "vwan-" + }, + "Microsoft.Network/vpnGateways": { + "friendlyName": "VPN Gateway", + "namingConvention": "vpng-" + }, + "Microsoft.Network/vpnGateways/vpnConnections": { + "friendlyName": "VPN connection", + "namingConvention": "vcn-" + }, + "Microsoft.Network/vpnGateways/vpnSites": { + "friendlyName": "VPN site", + "namingConvention": "vst-" + }, + "Microsoft.NotificationHubs/namespaces": { + "friendlyName": "Notification Hubs namespace", + "namingConvention": "ntfns-" + }, + "Microsoft.NotificationHubs/namespaces/notificationHubs": { + "friendlyName": "Notification Hubs", + "namingConvention": "ntf-" + }, + "Microsoft.OperationalInsights/workspaces": { + "friendlyName": "Log Analytics workspace", + "namingConvention": "log-" + }, + "Microsoft.PowerBIDedicated/capacities": { + "friendlyName": "Power BI Embedded", + "namingConvention": "pbi-" + }, + "Microsoft.Purview/accounts": { + "friendlyName": "Azure Purview instance", + "namingConvention": "pview-" + }, + "Microsoft.RecoveryServices/vaults": { + "friendlyName": "Recovery Services vault", + "namingConvention": "rsv-" + }, + "Microsoft.RecoveryServices/vaults/backupPolicies": { + "friendlyName": "Recovery Services vault backup policy", + "namingConvention": "rsvbp-" + }, + "Microsoft.Resources/resourceGroups": { + "friendlyName": "Resource group", + "namingConvention": "rg-" + }, + "Microsoft.Search/searchServices": { + "friendlyName": "Azure Cognitive Search", + "namingConvention": "srch-" + }, + "Microsoft.ServiceBus/namespaces": { + "friendlyName": "Service Bus", + "namingConvention": "sb-" + }, + "Microsoft.ServiceBus/namespaces/queues": { + "friendlyName": "Service Bus queue", + "namingConvention": "sbq-" + }, + "Microsoft.ServiceBus/namespaces/topics": { + "friendlyName": "Service Bus topic", + "namingConvention": "sbt-" + }, + "Microsoft.serviceEndPointPolicies": { + "friendlyName": "Service endpoint", + "namingConvention": "se-" + }, + "Microsoft.ServiceFabric/clusters": { + "friendlyName": "Service Fabric cluster", + "namingConvention": "sf-" + }, + "Microsoft.SignalRService/SignalR": { + "friendlyName": "SignalR", + "namingConvention": "sigr" + }, + "Microsoft.Sql/managedInstances": { + "friendlyName": "SQL Managed Instance", + "namingConvention": "sqlmi-" + }, + "Microsoft.Sql/servers": { + "friendlyName": [ + "Azure SQL Data Warehouse", + "Azure SQL Database server" + ], + "namingConvention": [ + "sqldw-", + "sql-" + ] + }, + "Microsoft.Sql/servers/databases": { + "friendlyName": [ + "SQL Server Stretch Database", + "Azure SQL database" + ], + "namingConvention": [ + "sqlstrdb-", + "sqldb-" + ] + }, + "Microsoft.Storage/storageAccounts": { + "friendlyName": [ + "Storage account", + "VM storage account" + ], + "namingConvention": [ + "st", + "stvm" + ] + }, + "Microsoft.StorSimple/managers": { + "friendlyName": "Azure StorSimple", + "namingConvention": "ssimp" + }, + "Microsoft.StreamAnalytics/cluster": { + "friendlyName": "Azure Stream Analytics", + "namingConvention": "asa-" + }, + "Microsoft.Synapse/workspaces": { + "friendlyName": [ + "Azure Synapse Analytics Workspaces", + "Azure Synapse Analytics" + ], + "namingConvention": [ + "synw", + "syn" + ] + }, + "Microsoft.Synapse/workspaces/sqlPools": { + "friendlyName": [ + "Azure Synapse Analytics Spark Pool", + "Azure Synapse Analytics SQL Dedicated Pool" + ], + "namingConvention": [ + "synsp", + "syndp" + ] + }, + "Microsoft.TimeSeriesInsights/environments": { + "friendlyName": "Time Series Insights environment", + "namingConvention": "tsi-" + }, + "Microsoft.Web/serverFarms": { + "friendlyName": "App Service plan", + "namingConvention": "plan-" + }, + "Microsoft.Web/sites": { + "friendlyName": [ + "Web app", + "Function app", + "App Service environment" + ], + "namingConvention": [ + "app-", + "func-", + "ase-" + ] + }, + "Microsoft.Web/staticSites": { + "friendlyName": "Static web app", + "namingConvention": "stapp-" + } + } +'@ + $htCAFNamingConvention = $JSONcafResourceNaming | ConvertFrom-Json + + $resourcesSubscriptionResultGroupedByType = $resourcesSubscriptionResult | Group-Object -Property type + foreach ($entry in $resourcesSubscriptionResultGroupedByType) { + + if ($htCAFNamingConvention.($entry.Name)) { + $doCAFResourceNamingCheck = $true + $namingConvention = $htCAFNamingConvention.($entry.Name).namingConvention + $namingConventionFriendlyName = $htCAFNamingConvention.($entry.Name).friendlyName + } + else { + $doCAFResourceNamingCheck = $false + $namingConvention = 'n/a' + $namingConventionFriendlyName = 'n/a' + } + + foreach ($resource in ($entry.Group)) { + + if ($doCAFResourceNamingCheck) { + $cafResourceNamingCheck = 'failed' + $applicableNaming = $namingConvention -join "$CsvDelimiterOpposite " + foreach ($naming in $namingConvention) { + if (($resource.name).StartsWith($naming, 'CurrentCultureIgnoreCase')) { + $cafResourceNamingCheck = 'passed' + #$applicableNaming = $naming + } + } + } + else { + $cafResourceNamingCheck = 'n/a' + $applicableNaming = 'n/a' + } + $null = $script:resourcesIdsAll.Add([PSCustomObject]@{ + subscriptionId = $scopeId + mgPath = $childMgMgPath + type = ($resource.type).ToLower() + id = ($resource.Id).ToLower() + name = ($resource.name).ToLower() + location = ($resource.location).ToLower() + tags = ($resource.tags) + createdTime = ($resource.createdTime) + changedTime = ($resource.changedTime) + cafResourceNamingResult = $cafResourceNamingCheck + cafResourceNaming = $applicableNaming + cafResourceNamingFriendlyName = $namingConventionFriendlyName -join "$CSVDelimiterOpposite " + }) + + if ($resource.identity.userAssignedIdentities) { + $resource.identity.userAssignedIdentities.psobject.properties | ForEach-Object { + if ((-not [string]::IsNullOrEmpty($resource.Id)) -and (-not [string]::IsNullOrEmpty($_.Value.principalId))) { + $hlp = ($_.Name.split('/')) + $hlpMiSubId = $hlp[2] + if ($scopeId -eq $hlpMiSubId) { + $miCrossSubscription = $false + } + else { + $miCrossSubscription = $true + } + $null = $script:arrayUserAssignedIdentities4Resources.Add([PSCustomObject]@{ + resourceId = $resource.Id + resourceName = $resource.name + resourceMgPath = $childMgMgPath + resourceSubscriptionName = $scopeDisplayName + resourceSubscriptionId = $scopeId + resourceResourceGroupName = ($resource.Id -split ('/'))[4] + resourceType = $resource.type + resourceLocation = $resource.location + miPrincipalId = $_.Value.principalId + miClientId = $_.Value.clientId + miMgPath = $htSubscriptionsMgPath.($hlpMiSubId).pathDelimited + miSubscriptionName = $htSubscriptionsMgPath.($hlpMiSubId).DisplayName + miSubscriptionId = $hlpMiSubId + miResourceGroupName = $hlp[4] + miResourceId = $_.Name + miResourceName = $_.Name -replace '.*/' + miCrossSubscription = $miCrossSubscription + }) + } + } + } + } + } + $endSubResourceIdsThis = Get-Date + $null = $script:arraySubResourcesAddArrayDuration.Add([PSCustomObject]@{ + sub = $scopeId + DurationSec = (New-TimeSpan -Start $startSubResourceIdsThis -End $endSubResourceIdsThis).TotalSeconds + }) + + + #resourceTags + $script:htSubscriptionTagList.($scopeId) = @{} + $script:htSubscriptionTagList.($scopeId).Resource = @{} + foreach ($tags in ($resourcesSubscriptionResult.where( { $_.Tags -and -not [String]::IsNullOrWhiteSpace($_.Tags) } )).Tags) { + foreach ($tagName in $tags.PSObject.Properties.Name) { + #resource + if ($htSubscriptionTagList.($scopeId).Resource.ContainsKey($tagName)) { + $script:htSubscriptionTagList.($scopeId).Resource."$tagName" += 1 + } + else { + $script:htSubscriptionTagList.($scopeId).Resource."$tagName" = 1 + } + + #resourceAll + if ($htAllTagList.Resource.ContainsKey($tagName)) { + $script:htAllTagList.Resource."$tagName" += 1 + } + else { + $script:htAllTagList.Resource."$tagName" = 1 + } + + #all + if ($htAllTagList.AllScopes.ContainsKey($tagName)) { + $script:htAllTagList.AllScopes."$tagName" += 1 + } + else { + $script:htAllTagList.AllScopes."$tagName" = 1 + } + } + } +} +$funcDataCollectionResources = $function:dataCollectionResources.ToString() + +function dataCollectionResourceGroups { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + #https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2020-06-01 + $currentTask = "Getting ResourceGroups for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/resourcegroups?api-version=2021-04-01" + $method = 'GET' + $resourceGroupsSubscriptionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $null = $script:resourceGroupsAll.Add([PSCustomObject]@{ + subscriptionId = $scopeId + count_ = ($resourceGroupsSubscriptionResult).count + }) + + #resourceGroupTags + if ($azAPICallConf['htParameters'].NoResources -eq $true) { + $script:htSubscriptionTagList.($scopeId) = @{} + } + + $script:htSubscriptionTagList.($scopeId).ResourceGroup = @{} + foreach ($tags in ($resourceGroupsSubscriptionResult.where( { $_.Tags -and -not [String]::IsNullOrWhiteSpace($_.Tags) } )).Tags) { + foreach ($tagName in $tags.PSObject.Properties.Name) { + + #resource + if ($htSubscriptionTagList.($scopeId).ResourceGroup.ContainsKey($tagName)) { + $script:htSubscriptionTagList.($scopeId).ResourceGroup."$tagName" += 1 + } + else { + $script:htSubscriptionTagList.($scopeId).ResourceGroup."$tagName" = 1 + } + + #resourceAll + if ($htAllTagList.ResourceGroup.ContainsKey($tagName)) { + $script:htAllTagList.ResourceGroup."$tagName" += 1 + } + else { + $script:htAllTagList.ResourceGroup."$tagName" = 1 + } + + #all + if ($htAllTagList.AllScopes.ContainsKey($tagName)) { + $script:htAllTagList.AllScopes."$tagName" += 1 + } + else { + $script:htAllTagList.AllScopes."$tagName" = 1 + } + } + } +} +$funcDataCollectionResourceGroups = $function:dataCollectionResourceGroups.ToString() + +function dataCollectionResourceProviders { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayname, + $subscriptionQuotaId + ) + + ($script:htResourceProvidersAll).($scopeId) = @{} + $currentTask = "Getting ResourceProviders for Subscription: '$($scopeDisplayname)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers?api-version=2019-10-01" + $method = 'GET' + $resProvResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + ($script:htResourceProvidersAll).($scopeId).Providers = $resProvResult | Select-Object namespace, registrationState +} +$funcDataCollectionResourceProviders = $function:dataCollectionResourceProviders.ToString() + +function dataCollectionFeatures { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayname, + [object]$MgParentNameChain, + $subscriptionQuotaId + ) + + $currentTask = "Getting Features for Subscription: '$($scopeDisplayname)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Features/features?api-version=2021-07-01" + $method = 'GET' + $featuresResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $featuresResultRegistered = $featuresResult.where({ $_.properties.state -eq 'Registered' }) + + if ($featuresResultRegistered.Count -gt 0) { + foreach ($registeredFeature in $featuresResultRegistered) { + $null = $script:arrayFeaturesAll.Add([PSCustomObject]@{ + subscriptionId = $registeredFeature.id.split('/')[2] + mgPathArray = $MgParentNameChain + mgPath = ($MgParentNameChain -join ',') + feature = $registeredFeature.name + }) + } + } +} +$funcDataCollectionFeatures = $function:dataCollectionFeatures.ToString() + +function dataCollectionResourceLocks { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayname, + $subscriptionQuotaId + ) + + $currentTask = "Getting ResourceLocks for Subscription: '$($scopeDisplayname)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/locks?api-version=2016-09-01" + $method = 'GET' + $requestSubscriptionResourceLocks = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $requestSubscriptionResourceLocksCount = ($requestSubscriptionResourceLocks).Count + if ($requestSubscriptionResourceLocksCount -gt 0) { + $htTemp = @{} + $locksAnyLockSubscriptionCount = 0 + $locksCannotDeleteSubscriptionCount = 0 + $locksReadOnlySubscriptionCount = 0 + $arrayResourceGroupsAnyLock = [System.Collections.ArrayList]@() + $arrayResourceGroupsCannotDeleteLock = [System.Collections.ArrayList]@() + $arrayResourceGroupsReadOnlyLock = [System.Collections.ArrayList]@() + $arrayResourcesAnyLock = [System.Collections.ArrayList]@() + $arrayResourcesCannotDeleteLock = [System.Collections.ArrayList]@() + $arrayResourcesReadOnlyLock = [System.Collections.ArrayList]@() + foreach ($requestSubscriptionResourceLock in $requestSubscriptionResourceLocks) { + + $splitRequestSubscriptionResourceLockId = ($requestSubscriptionResourceLock.Id).Split('/') + switch (($splitRequestSubscriptionResourceLockId).Count - 1) { + #subLock + 6 { + $locksAnyLockSubscriptionCount++ + if ($requestSubscriptionResourceLock.properties.level -eq 'CanNotDelete') { + $locksCannotDeleteSubscriptionCount++ + } + if ($requestSubscriptionResourceLock.properties.level -eq 'ReadOnly') { + $locksReadOnlySubscriptionCount++ + } + } + #rgLock + 8 { + $resourceGroupName = $splitRequestSubscriptionResourceLockId[0..4] -join '/' + $null = $arrayResourceGroupsAnyLock.Add([PSCustomObject]@{ + rg = $resourceGroupName + }) + if ($requestSubscriptionResourceLock.properties.level -eq 'CanNotDelete') { + $null = $arrayResourceGroupsCannotDeleteLock.Add([PSCustomObject]@{ + rg = $resourceGroupName + }) + } + if ($requestSubscriptionResourceLock.properties.level -eq 'ReadOnly') { + $null = $arrayResourceGroupsReadOnlyLock.Add([PSCustomObject]@{ + rg = $resourceGroupName + }) + } + } + #resLock + 12 { + $resourceId = $splitRequestSubscriptionResourceLockId[0..8] -join '/' + $null = $arrayResourcesAnyLock.Add([PSCustomObject]@{ + res = $resourceId + }) + if ($requestSubscriptionResourceLock.properties.level -eq 'CanNotDelete') { + $null = $arrayResourcesCannotDeleteLock.Add([PSCustomObject]@{ + res = $resourceId + }) + } + if ($requestSubscriptionResourceLock.properties.level -eq 'ReadOnly') { + $null = $arrayResourcesReadOnlyLock.Add([PSCustomObject]@{ + res = $resourceId + }) + } + } + } + } + + $htTemp.SubscriptionLocksCannotDeleteCount = $locksCannotDeleteSubscriptionCount + $htTemp.SubscriptionLocksReadOnlyCount = $locksReadOnlySubscriptionCount + + #resourceGroups + $htTemp.ResourceGroupsLocksCannotDeleteCount = $arrayResourceGroupsCannotDeleteLock.Count + $htTemp.ResourceGroupsLocksCannotDelete = $arrayResourceGroupsCannotDeleteLock + + $htTemp.ResourceGroupsLocksReadOnlyCount = $arrayResourceGroupsReadOnlyLock.Count + $htTemp.ResourceGroupsLocksReadOnly = $arrayResourceGroupsReadOnlyLock + + #resources + $htTemp.ResourcesLocksCannotDeleteCount = $arrayResourcesCannotDeleteLock.Count + $htTemp.ResourcesLocksCannotDelete = $arrayResourcesCannotDeleteLock + + $htTemp.ResourcesLocksReadOnlyCount = $arrayResourcesReadOnlyLock.Count + $htTemp.ResourcesLocksReadOnly = $arrayResourcesReadOnlyLock + + $script:htResourceLocks.($scopeId) = $htTemp + } +} +$funcDataCollectionResourceLocks = $function:dataCollectionResourceLocks.ToString() + +function dataCollectionTags { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + $currentTask = "Getting Tags for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Resources/tags/default?api-version=2020-06-01" + $method = 'GET' + $requestSubscriptionTags = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' -caller 'CustomDataCollection' + + $script:htSubscriptionTagList.($scopeId).Subscription = @{} + if ($requestSubscriptionTags.properties.tags) { + $subscriptionTags = @() + ($script:htSubscriptionTags).($scopeId) = @{} + foreach ($tag in ($requestSubscriptionTags.properties.tags).PSObject.Properties) { + $subscriptionTags += "$($tag.Name)/$($tag.Value)" + + ($script:htSubscriptionTags).($scopeId).($tag.Name) = $tag.Value + $tagName = $tag.Name + + #subscription + if ($htSubscriptionTagList.($scopeId).Subscription.ContainsKey($tagName)) { + $script:htSubscriptionTagList.($scopeId).Subscription."$tagName" += 1 + } + else { + $script:htSubscriptionTagList.($scopeId).Subscription."$tagName" = 1 + } + + #subscriptionAll + if ($htAllTagList.Subscription.ContainsKey($tagName)) { + $script:htAllTagList.Subscription."$tagName" += 1 + } + else { + $script:htAllTagList.Subscription."$tagName" = 1 + } + + #all + if ($htAllTagList.AllScopes.ContainsKey($tagName)) { + $script:htAllTagList.AllScopes."$tagName" += 1 + } + else { + $script:htAllTagList.AllScopes."$tagName" = 1 + } + + } + $subscriptionTagsCount = ($subscriptionTags).Count + $subscriptionTags = $subscriptionTags -join "$CsvDelimiterOpposite " + } + else { + $subscriptionTagsCount = 0 + $subscriptionTags = 'none' + } + $htSubscriptionTagsReturn = @{} + $htSubscriptionTagsReturn.subscriptionTagsCount = $subscriptionTagsCount + $htSubscriptionTagsReturn.subscriptionTags = $subscriptionTags + return $htSubscriptionTagsReturn +} +$funcDataCollectionTags = $function:dataCollectionTags.ToString() + +function dataCollectionPolicyComplianceStates { + [CmdletBinding()]Param( + [string]$TargetMgOrSub, + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + + if ($TargetMgOrSub -eq 'Sub') { + $currentTask = "Getting Policy Compliance for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.PolicyInsights/policyStates/latest/summarize?api-version=2019-10-01" + } + if ($TargetMgOrSub -eq 'MG') { + $currentTask = "Getting Policy Compliance for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.PolicyInsights/policyStates/latest/summarize?api-version=2019-10-01" + } + $method = 'POST' + $policyComplianceResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($policyComplianceResult -eq 'ResponseTooLarge') { + if ($TargetMgOrSub -eq 'Sub') { + $script:htCachePolicyComplianceResponseTooLargeSUB.($scopeId) = @{} + } + if ($TargetMgOrSub -eq 'MG') { + $script:htCachePolicyComplianceResponseTooLargeMG.($scopeId) = @{} + } + } + elseif ($policyComplianceResult -eq 'DisallowedProvider') { + #nothing to do + } + else { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId) = @{} } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId) = @{} } + foreach ($policyAssignment in $policyComplianceResult.policyassignments | Sort-Object -Property policyAssignmentId) { + $policyAssignmentIdToLower = ($policyAssignment.policyAssignmentId).ToLower() + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower) = @{} } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower) = @{} } + foreach ($policyComplianceState in $policyAssignment.results.policydetails) { + if ($policyComplianceState.ComplianceState -eq 'compliant') { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower).CompliantPolicies = $policyComplianceState.count } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower).CompliantPolicies = $policyComplianceState.count } + } + if ($policyComplianceState.ComplianceState -eq 'noncompliant') { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower).NonCompliantPolicies = $policyComplianceState.count } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower).NonCompliantPolicies = $policyComplianceState.count } + } + } + + foreach ($resourceComplianceState in $policyAssignment.results.resourcedetails) { + if ($resourceComplianceState.ComplianceState -eq 'compliant') { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower).CompliantResources = $resourceComplianceState.count } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower).CompliantResources = $resourceComplianceState.count } + + } + if ($resourceComplianceState.ComplianceState -eq 'nonCompliant') { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower).NonCompliantResources = $resourceComplianceState.count } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower).NonCompliantResources = $resourceComplianceState.count } + + } + if ($resourceComplianceState.ComplianceState -eq 'conflict') { + if ($TargetMgOrSub -eq 'Sub') { ($script:htCachePolicyComplianceSUB).($scopeId).($policyAssignmentIdToLower).ConflictingResources = $resourceComplianceState.count } + if ($TargetMgOrSub -eq 'MG') { ($script:htCachePolicyComplianceMG).($scopeId).($policyAssignmentIdToLower).ConflictingResources = $resourceComplianceState.count } + } + } + } + } +} +$funcDataCollectionPolicyComplianceStates = $function:dataCollectionPolicyComplianceStates.ToString() + +function dataCollectionASCSecureScoreSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) { + $currentTask = "Getting Microsoft Defender for Cloud Secure Score for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Security/securescores?api-version=2020-01-01" + $method = 'GET' + $subASCSecureScoreResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($subASCSecureScoreResult -ne 'DisallowedProvider') { + $subASCSecureScoreResultASCScore = ($subASCSecureScoreResult.where({ $_.name -eq 'ascScore' })) + if ($subASCSecureScoreResultASCScore.count -gt 0) { + $secureScorePercentageRounded = [math]::Round(($subASCSecureScoreResultASCScore.properties.score.current / $subASCSecureScoreResultASCScore.properties.score.max * 100), 2) + $subscriptionASCSecureScore = "$($secureScorePercentageRounded)% ($($subASCSecureScoreResultASCScore.properties.score.current) of $($subASCSecureScoreResultASCScore.properties.score.max) points)" + } + else { + $subscriptionASCSecureScore = 'n/a' + } + } + else { + $subscriptionASCSecureScore = 'n/a' + } + + } + else { + $subscriptionASCSecureScore = "excluded (-NoMDfCSecureScore $($azAPICallConf['htParameters'].NoMDfCSecureScore))" + } + return $subscriptionASCSecureScore +} +$funcDataCollectionASCSecureScoreSub = $function:dataCollectionASCSecureScoreSub.ToString() + +function dataCollectionBluePrintDefinitionsMG { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $mgParentId, + $mgParentName, + $mgAscSecureScoreResult + ) + + $currentTask = "Getting Blueprint definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Blueprint/blueprints?api-version=2018-11-01-preview" + $method = 'GET' + $scopeBlueprintDefinitionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $addRowToTableDone = $false + if (($scopeBlueprintDefinitionResult).count -gt 0) { + foreach ($blueprint in $scopeBlueprintDefinitionResult) { + + if (-not $($htCacheDefinitionsBlueprint).($blueprint.Id)) { + ($script:htCacheDefinitionsBlueprint).($blueprint.Id) = @{} + } + + $blueprintName = $blueprint.name + $blueprintId = $blueprint.Id + $blueprintDisplayName = $blueprint.properties.displayName + $blueprintDescription = $blueprint.properties.description + $blueprintScoped = "/providers/Microsoft.Management/managementGroups/$($scopeId)" + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $scopeDisplayName ` + -mgId $scopeId ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -BlueprintName $blueprintName ` + -BlueprintId $blueprintId ` + -BlueprintDisplayName $blueprintDisplayName ` + -BlueprintDescription $blueprintDescription ` + -BlueprintScoped $blueprintScoped + } + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionBluePrintDefinitionsMG = $function:dataCollectionBluePrintDefinitionsMG.ToString() + +function dataCollectionBluePrintDefinitionsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $childMgDisplayName, + $childMgId, + $childMgParentId, + $childMgParentName, + $mgAscSecureScoreResult, + $subscriptionQuotaId, + $subscriptionState, + $subscriptionASCSecureScore, + $subscriptionTags, + $subscriptionTagsCount + ) + + $currentTask = "Getting Blueprint definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Blueprint/blueprints?api-version=2018-11-01-preview" + $method = 'GET' + $scopeBlueprintDefinitionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $addRowToTableDone = $false + if ($scopeBlueprintDefinitionResult -ne 'DisallowedProvider') { + if (($scopeBlueprintDefinitionResult).count -gt 0) { + foreach ($blueprint in $scopeBlueprintDefinitionResult) { + + if (-not $($htCacheDefinitionsBlueprint).($blueprint.Id)) { + ($script:htCacheDefinitionsBlueprint).($blueprint.Id) = @{} + } + + $blueprintName = $blueprint.name + $blueprintId = $blueprint.Id + $blueprintDisplayName = $blueprint.properties.displayName + $blueprintDescription = $blueprint.properties.description + $blueprintScoped = "/subscriptions/$($scopeId)" + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $scopeDisplayName ` + -SubscriptionId $scopeId ` + -SubscriptionQuotaId $subscriptionQuotaId ` + -SubscriptionState $subscriptionState ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore ` + -SubscriptionTags $subscriptionTags ` + -SubscriptionTagsCount $subscriptionTagsCount ` + -BlueprintName $blueprintName ` + -BlueprintId $blueprintId ` + -BlueprintDisplayName $blueprintDisplayName ` + -BlueprintDescription $blueprintDescription ` + -BlueprintScoped $blueprintScoped + } + } + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionBluePrintDefinitionsSub = $function:dataCollectionBluePrintDefinitionsSub.ToString() + +function dataCollectionBluePrintAssignmentsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $childMgDisplayName, + $childMgId, + $childMgParentId, + $childMgParentName, + $mgAscSecureScoreResult, + $subscriptionQuotaId, + $subscriptionState, + $subscriptionASCSecureScore, + $subscriptionTags, + $subscriptionTagsCount + ) + + $currentTask = "Getting Blueprint assignments for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Blueprint/blueprintAssignments?api-version=2018-11-01-preview" + $method = 'GET' + $subscriptionBlueprintAssignmentsResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $addRowToTableDone = $false + if ($subscriptionBlueprintAssignmentsResult -ne 'DisallowedProvider') { + if (($subscriptionBlueprintAssignmentsResult).count -gt 0) { + foreach ($subscriptionBlueprintAssignment in $subscriptionBlueprintAssignmentsResult) { + + if (-not ($htCacheAssignmentsBlueprint).($subscriptionBlueprintAssignment.Id)) { + ($script:htCacheAssignmentsBlueprint).($subscriptionBlueprintAssignment.Id) = @{} + ($script:htCacheAssignmentsBlueprint).($subscriptionBlueprintAssignment.Id) = $subscriptionBlueprintAssignment + } + + if (($subscriptionBlueprintAssignment.properties.blueprintId) -like '/subscriptions/*') { + $blueprintScope = $subscriptionBlueprintAssignment.properties.blueprintId -replace '/providers/Microsoft.Blueprint/blueprints/.*', '' + $blueprintName = $subscriptionBlueprintAssignment.properties.blueprintId -replace '.*/blueprints/', '' -replace '/versions/.*', '' + } + if (($subscriptionBlueprintAssignment.properties.blueprintId) -like '/providers/Microsoft.Management/managementGroups/*') { + $blueprintScope = $subscriptionBlueprintAssignment.properties.blueprintId -replace '/providers/Microsoft.Blueprint/blueprints/.*', '' + $blueprintName = $subscriptionBlueprintAssignment.properties.blueprintId -replace '.*/blueprints/', '' -replace '/versions/.*', '' + } + + $currentTask = "Getting Blueprint definitions related to Blueprint assignments for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/$($blueprintScope)/providers/Microsoft.Blueprint/blueprints/$($blueprintName)?api-version=2018-11-01-preview" + $method = 'GET' + $subscriptionBlueprintDefinitionResult = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' -caller 'CustomDataCollection' + + if ($subscriptionBlueprintDefinitionResult -eq 'BlueprintNotFound') { + $blueprintName = 'BlueprintNotFound' + $blueprintId = 'BlueprintNotFound' + $blueprintAssignmentVersion = $subscriptionBlueprintAssignment.properties.blueprintId -replace '.*/' + $blueprintDisplayName = 'BlueprintNotFound' + $blueprintDescription = 'BlueprintNotFound' + $blueprintScoped = $blueprintScope + $blueprintAssignmentId = $subscriptionBlueprintAssignmentsResult.Id + } + else { + $blueprintName = $subscriptionBlueprintDefinitionResult.name + $blueprintId = $subscriptionBlueprintDefinitionResult.Id + $blueprintAssignmentVersion = $subscriptionBlueprintAssignment.properties.blueprintId -replace '.*/' + $blueprintDisplayName = $subscriptionBlueprintDefinitionResult.properties.displayName + $blueprintDescription = $subscriptionBlueprintDefinitionResult.properties.description + $blueprintScoped = $blueprintScope + $blueprintAssignmentId = $subscriptionBlueprintAssignmentsResult.Id + } + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $scopeDisplayName ` + -SubscriptionId $scopeId ` + -SubscriptionQuotaId $subscriptionQuotaId ` + -SubscriptionState $subscriptionState ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore ` + -SubscriptionTags $subscriptionTags ` + -SubscriptionTagsCount $subscriptionTagsCount ` + -BlueprintName $blueprintName ` + -BlueprintId $blueprintId ` + -BlueprintDisplayName $blueprintDisplayName ` + -BlueprintDescription $blueprintDescription ` + -BlueprintScoped $blueprintScoped ` + -BlueprintAssignmentVersion $blueprintAssignmentVersion ` + -BlueprintAssignmentId $blueprintAssignmentId + } + } + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionBluePrintAssignmentsSub = $function:dataCollectionBluePrintAssignmentsSub.ToString() + +function dataCollectionPolicyExemptions { + [CmdletBinding()]Param( + [string]$TargetMgOrSub, + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + if ($TargetMgOrSub -eq 'Sub') { + $currentTask = "Getting Policy exemptions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/policyExemptions?api-version=2020-07-01-preview" + } + if ($TargetMgOrSub -eq 'MG') { + $currentTask = "Getting Policy exemptions for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/policyExemptions?api-version=2020-07-01-preview&`$filter=atScope()" + } + $method = 'GET' + $requestPolicyExemptionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $requestPolicyExemptionAPICount = ($requestPolicyExemptionAPI).Count + if ($requestPolicyExemptionAPICount -gt 0) { + foreach ($exemption in $requestPolicyExemptionAPI) { + if (-not $htPolicyAssignmentExemptions.($exemption.Id)) { + $script:htPolicyAssignmentExemptions.($exemption.Id) = @{} + $script:htPolicyAssignmentExemptions.($exemption.Id).exemption = $exemption + } + } + } +} +$funcDataCollectionPolicyExemptions = $function:dataCollectionPolicyExemptions.ToString() + +function dataCollectionPolicyDefinitions { + [CmdletBinding()]Param( + [string]$TargetMgOrSub, + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + if ($TargetMgOrSub -eq 'Sub') { + $currentTask = "Getting Policy definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Custom'" + } + if ($TargetMgOrSub -eq 'MG') { + $currentTask = "Getting Policy definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($scopeId)/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Custom'" + } + $method = 'GET' + $requestPolicyDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $scopePolicyDefinitions = $requestPolicyDefinitionAPI.where( { $_.properties.policyType -eq 'custom' } ) + + if ($TargetMgOrSub -eq 'Sub') { + $PolicyDefinitionsScopedCount = (($scopePolicyDefinitions.where( { ($_.id) -like "/subscriptions/$($scopeId)/*" } ))).count + } + if ($TargetMgOrSub -eq 'MG') { + $PolicyDefinitionsScopedCount = (($scopePolicyDefinitions.where( { ($_.id) -like "/providers/Microsoft.Management/managementGroups/$($scopeId)/*" } ))).count + } + + foreach ($scopePolicyDefinition in $scopePolicyDefinitions) { + $hlpPolicyDefinitionId = ($scopePolicyDefinition.id).ToLower() + + $doIt = $true + if ($TargetMgOrSub -eq 'MG') { + $doIt = $false + if ($hlpPolicyDefinitionId -like "/providers/Microsoft.Management/managementGroups/$($scopeId)/*" -and $hlpPolicyDefinitionId -notlike "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/*") { + $doIt = $true + } + if ($scopeId -eq $ManagementGroupId) { + $doIt = $true + } + } + + if ($doIt) { + + if (-not $script:htCacheDefinitionsPolicy.($hlpPolicyDefinitionId)) { + if (($scopePolicyDefinition.Properties.description).length -eq 0) { + $policyDefinitionDescription = 'no description given' + } + else { + $policyDefinitionDescription = $scopePolicyDefinition.Properties.description + } + + $htTemp = @{} + $htTemp.Id = $hlpPolicyDefinitionId + + if ($hlpPolicyDefinitionId -like '/providers/Microsoft.Management/managementGroups/*') { + $htTemp.Scope = (($hlpPolicyDefinitionId).split('/'))[0..4] -join '/' + $htTemp.ScopeMgSub = 'Mg' + $htTemp.ScopeId = (($hlpPolicyDefinitionId).split('/'))[4] + $htTemp.ScopeMGLevel = $htManagementGroupsMgPath.((($hlpPolicyDefinitionId).split('/'))[4]).ParentNameChainCount + } + + if ($hlpPolicyDefinitionId -like '/subscriptions/*') { + $htTemp.Scope = (($hlpPolicyDefinitionId).split('/'))[0..2] -join '/' + $htTemp.ScopeMgSub = 'Sub' + $htTemp.ScopeId = (($hlpPolicyDefinitionId).split('/'))[2] + $htTemp.ScopeMGLevel = $htSubscriptionsMgPath.((($hlpPolicyDefinitionId).split('/'))[2]).level + } + + + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { + + $policyJsonRule = $scopePolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99 + $hash = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonRule)) + $stringHash = [System.BitConverter]::ToString($hash) + + if ($alzPolicies.($scopePolicyDefinition.name) -or $alzPolicyHashes.($stringHash) -or $scopePolicyDefinition.properties.metadata.source -eq 'https://github.com/Azure/Enterprise-Scale/') { + + $policyHashMatch = $false + if ($alzPolicyHashes.($stringHash)) { + $policyHashMatch = $true + $htTemp.ALZ = 'true' + if ($alzPolicyHashes.($stringHash).metadataSource -eq 'https://github.com/Azure/Enterprise-Scale/' -and $alzPolicyHashes.($stringHash).metadataSource -eq $scopePolicyDefinition.properties.metadata.source -and $alzPolicyHashes.($stringHash).policyName -eq $scopePolicyDefinition.name) { + $htTemp.ALZIdentificationLevel = 'PolicyRule Hash, Policy Name, MetaData Tag' + } + elseif ($alzPolicyHashes.($stringHash).policyName -eq $scopePolicyDefinition.name) { + $htTemp.ALZIdentificationLevel = 'PolicyRule Hash, Policy Name' + } + else { + $htTemp.ALZIdentificationLevel = 'PolicyRule Hash' + } + $htTemp.ALZPolicyName = $alzPolicyHashes.($stringHash).policyName + $htTemp.hash = $stringHash + if ($alzpolicies.($alzPolicyHashes.($stringHash).policyName).status -eq 'obsolete') { + $htTemp.ALZState = 'obsolete' + $htTemp.ALZLatestVer = '' + } + else { + if ($scopePolicyDefinition.Properties.metadata.version) { + if ($alzpolicies.($alzPolicyHashes.($stringHash).policyName).latestVersion -eq $scopePolicyDefinition.Properties.metadata.version) { + $htTemp.ALZState = 'upToDate' + } + else { + if ($alzpolicies.($alzPolicyHashes.($stringHash).policyName).latestVersion -like '*-deprecated') { + $htTemp.ALZState = 'deprecated' + } + else { + $htTemp.ALZState = 'outDated' + } + } + } + else { + $htTemp.ALZState = 'potentiallyOutDated (no ver)' + } + $htTemp.ALZLatestVer = $alzpolicies.($alzPolicyHashes.($stringHash).policyName).latestVersion + } + } + + $policyNameMatch = $false + if ($alzPolicies.($scopePolicyDefinition.name) -and -not $policyHashMatch) { + $policyNameMatch = $true + $htTemp.ALZ = 'true' + if ($alzPolicies.($scopePolicyDefinition.name).metadataSource -eq 'https://github.com/Azure/Enterprise-Scale/' -and $alzPolicies.($scopePolicyDefinition.name).metadataSource -eq $scopePolicyDefinition.properties.metadata.source) { + $htTemp.ALZIdentificationLevel = 'Policy Name, MetaData Tag' + } + else { + $htTemp.ALZIdentificationLevel = 'Policy Name' + } + + $htTemp.ALZPolicyName = $alzPolicies.($scopePolicyDefinition.name).policyName + $htTemp.hash = $stringHash + if ($alzPolicies.($scopePolicyDefinition.name).status -eq 'obsolete') { + $htTemp.ALZState = 'obsolete' + $htTemp.ALZLatestVer = '' + } + else { + if ($scopePolicyDefinition.Properties.metadata.version) { + if ($alzPolicies.($scopePolicyDefinition.name).latestVersion -eq $scopePolicyDefinition.Properties.metadata.version) { + $htTemp.ALZState = 'upToDate' + } + else { + if ($alzPolicies.($scopePolicyDefinition.name).latestVersion -like '*-deprecated') { + $htTemp.ALZState = 'deprecated' + } + else { + $htTemp.ALZState = 'outDated' + } + } + } + else { + $htTemp.ALZState = 'potentiallyOutDated (no ver)' + } + + $htTemp.ALZLatestVer = $alzPolicies.($scopePolicyDefinition.name).latestVersion + } + } + + if ($scopePolicyDefinition.properties.metadata.source -eq 'https://github.com/Azure/Enterprise-Scale/' -and -not $policyHashMatch -and -not $policyNameMatch) { + $htTemp.ALZ = 'true' + $htTemp.ALZState = 'unknown' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = 'MetaData Tag' + $htTemp.ALZPolicyName = '' + $htTemp.hash = $stringHash + } + } + else { + $htTemp.ALZ = 'false' + $htTemp.ALZState = '' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = '' + $htTemp.ALZPolicyName = '' + $htTemp.hash = $stringHash + } + } + else { + $htTemp.ALZ = 'n/a' + $htTemp.ALZState = '' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = '' + $htTemp.ALZPolicyName = '' + $htTemp.hash = '' + } + + $htTemp.DisplayName = $($scopePolicyDefinition.Properties.displayname) + $htTemp.Name = $scopePolicyDefinition.Name + $htTemp.Description = $($policyDefinitionDescription) + $htTemp.Type = $($scopePolicyDefinition.Properties.policyType) + $htTemp.Category = $($scopePolicyDefinition.Properties.metadata.category) + if ($scopePolicyDefinition.Properties.metadata.version) { + $htTemp.Version = $($scopePolicyDefinition.Properties.metadata.version) + } + else { + $htTemp.Version = 'n/a' + } + $htTemp.PolicyDefinitionId = $hlpPolicyDefinitionId + if ($scopePolicyDefinition.Properties.metadata.deprecated -eq $true -or $scopePolicyDefinition.Properties.displayname -like "``[Deprecated``]*") { + $htTemp.Deprecated = $scopePolicyDefinition.Properties.metadata.deprecated + } + else { + $htTemp.Deprecated = $false + } + if ($scopePolicyDefinition.Properties.metadata.preview -eq $true -or $scopePolicyDefinition.Properties.displayname -like "``[*Preview``]*") { + $htTemp.Preview = $scopePolicyDefinition.Properties.metadata.preview + } + else { + $htTemp.Preview = $false + } + + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $scopePolicyDefinition + $htTemp.effectDefaultValue = $htEffectDetected.defaultValue + $htTemp.effectAllowedValue = $htEffectDetected.allowedValues + $htTemp.effectFixedValue = $htEffectDetected.fixedValue + #endregion effect + + $htTemp.Json = $scopePolicyDefinition + $script:htCacheDefinitionsPolicy.($hlpPolicyDefinitionId) = $htTemp + } + + + if (-not [string]::IsNullOrWhiteSpace($scopePolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { + $script:htCacheDefinitionsPolicy.($hlpPolicyDefinitionId).RoleDefinitionIds = $scopePolicyDefinition.properties.policyRule.then.details.roleDefinitionIds + foreach ($roledefinitionId in $scopePolicyDefinition.properties.policyRule.then.details.roleDefinitionIds) { + if (-not [string]::IsNullOrEmpty($roledefinitionId)) { + if (-not $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId)) { + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId) = @{} + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = [array]$hlpPolicyDefinitionId + } + else { + $usedInPolicies = $htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies + $usedInPolicies += $hlpPolicyDefinitionId + $script:htRoleDefinitionIdsUsedInPolicy.($roledefinitionId).UsedInPolicies = $usedInPolicies + } + } + else { + Write-Host "$currentTask $($hlpPolicyDefinitionId) Finding: empty roleDefinitionId in roledefinitionIds" + } + } + } + else { + $script:htCacheDefinitionsPolicy.($hlpPolicyDefinitionId).RoleDefinitionIds = 'n/a' + } + + #region namingValidation + if (-not [string]::IsNullOrEmpty($scopePolicyDefinition.Properties.displayname)) { + $namingValidationResult = NamingValidation -toCheck $scopePolicyDefinition.Properties.displayname + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.Policy.($hlpPolicyDefinitionId)) { + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId) = @{} + } + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId).displayNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId).displayName = $scopePolicyDefinition.Properties.displayname + } + } + if (-not [string]::IsNullOrEmpty($scopePolicyDefinition.Name)) { + $namingValidationResult = NamingValidation -toCheck $scopePolicyDefinition.Name + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.Policy.($hlpPolicyDefinitionId)) { + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId) = @{} + } + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.Policy.($hlpPolicyDefinitionId).name = $scopePolicyDefinition.Name + } + } + #endregion namingValidation + } + } + + $returnObject = @{} + $returnObject.'PolicyDefinitionsScopedCount' = $PolicyDefinitionsScopedCount + return $returnObject +} +$funcDataCollectionPolicyDefinitions = $function:dataCollectionPolicyDefinitions.ToString() + +function dataCollectionPolicySetDefinitions { + [CmdletBinding()]Param( + [string]$TargetMgOrSub, + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + if ($TargetMgOrSub -eq 'Sub') { + $currentTask = "Getting PolicySet definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/policySetDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Custom'" + } + if ($TargetMgOrSub -eq 'MG') { + $currentTask = "Getting PolicySet definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($scopeId)/providers/Microsoft.Authorization/policySetDefinitions?api-version=2021-06-01&`$filter=policyType eq 'Custom'" + } + $method = 'GET' + $requestPolicySetDefinitionAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $scopePolicySetDefinitions = $requestPolicySetDefinitionAPI.where( { $_.properties.policyType -eq 'custom' } ) + if ($TargetMgOrSub -eq 'Sub') { + $PolicySetDefinitionsScopedCount = ($scopePolicySetDefinitions.where( { ($_.Id) -like "/subscriptions/$($scopeId)/*" } )).count + } + if ($TargetMgOrSub -eq 'MG') { + $PolicySetDefinitionsScopedCount = (($scopePolicySetDefinitions.where( { ($_.Id) -like "/providers/Microsoft.Management/managementGroups/$($scopeId)/*" } ))).count + } + + foreach ($scopePolicySetDefinition in $scopePolicySetDefinitions) { + $hlpPolicySetDefinitionId = ($scopePolicySetDefinition.id).ToLower() + + $doIt = $true + if ($TargetMgOrSub -eq 'MG') { + $doIt = $false + if ($hlpPolicySetDefinitionId -like "/providers/Microsoft.Management/managementGroups/$($scopeId)/*" -and $hlpPolicySetDefinitionId -notlike "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)/*") { + $doIt = $true + } + if ($scopeId -eq $ManagementGroupId) { + $doIt = $true + } + } + + if ($doIt) { + if (-not $script:htCacheDefinitionsPolicySet.($hlpPolicySetDefinitionId)) { + if (($scopePolicySetDefinition.Properties.description).length -eq 0) { + $policySetDefinitionDescription = 'no description given' + } + else { + $policySetDefinitionDescription = $scopePolicySetDefinition.Properties.description + } + + $htTemp = @{} + $htTemp.Id = $hlpPolicySetDefinitionId + if ($scopePolicySetDefinition.Id -like '/providers/Microsoft.Management/managementGroups/*') { + $htTemp.Scope = (($scopePolicySetDefinition.Id).split('/'))[0..4] -join '/' + $htTemp.ScopeMgSub = 'Mg' + $htTemp.ScopeId = (($scopePolicySetDefinition.Id).split('/'))[4] + $htTemp.ScopeMGLevel = $htManagementGroupsMgPath.((($scopePolicySetDefinition.Id).split('/'))[4]).ParentNameChainCount + } + + if ($scopePolicySetDefinition.Id -like '/subscriptions/*') { + $htTemp.Scope = (($scopePolicySetDefinition.Id).split('/'))[0..2] -join '/' + $htTemp.ScopeMgSub = 'Sub' + $htTemp.ScopeId = (($scopePolicySetDefinition.Id).split('/'))[2] + $htTemp.ScopeMGLevel = $htSubscriptionsMgPath.((($scopePolicySetDefinition.Id).split('/'))[2]).level + } + + if ($azAPICallConf['htParameters'].NoALZPolicyVersionChecker -eq $false) { + + $policyJsonParameters = $scopePolicySetDefinition.properties.parameters | ConvertTo-Json -Depth 99 + $policyJsonPolicyDefinitions = $scopePolicySetDefinition.properties.policyDefinitions | ConvertTo-Json -Depth 99 + $hashParameters = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonParameters)) + $stringHashParameters = [System.BitConverter]::ToString($hashParameters) + $hashPolicyDefinitions = [System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($policyJsonPolicyDefinitions)) + $stringHashPolicyDefinitions = [System.BitConverter]::ToString($hashPolicyDefinitions) + $stringHash = "$($stringHashParameters)_$($stringHashPolicyDefinitions)" + + if ($alzPolicySets.($scopePolicySetDefinition.name) -or $allESLZPolicySetHashes.($stringHash) -or $scopePolicySetDefinition.properties.metadata.source -eq 'https://github.com/Azure/Enterprise-Scale/') { + + $policySetHashMatch = $false + if ($alzPolicySetHashes.($stringHash)) { + $policySetHashMatch = $true + $htTemp.ALZ = 'true' + if ($allESLZPolicySetHashes.($stringHash).metadataSource -eq 'https://github.com/Azure/Enterprise-Scale/' -and $allESLZPolicySetHashes.($stringHash).metadataSource -eq $scopePolicySetDefinition.properties.metadata.source -and $allESLZPolicySetHashes.($stringHash).policySetName -eq $scopePolicySetDefinition.name) { + $htTemp.ALZIdentificationLevel = 'PolicySet Hash, PolicySet Name, MetaData Tag' + } + elseif ($allESLZPolicySetHashes.($stringHash).policySetName -eq $scopePolicySetDefinition.name) { + $htTemp.ALZIdentificationLevel = 'PolicySet Hash, PolicySet Name' + } + else { + $htTemp.ALZIdentificationLevel = 'PolicySet Hash' + } + $htTemp.ALZPolicySetName = $alzPolicySetHashes.($stringHash).policySetName + if ($alzPolicySetHashes.($stringHash).status -eq 'obsolete') { + $htTemp.ALZState = 'obsolete' + $htTemp.ALZLatestVer = '' + } + else { + if ($alzPolicySetHashes.($stringHash).latestVersion -eq $scopePolicySetDefinition.Properties.metadata.version) { + $htTemp.ALZState = 'upToDate' + } + else { + if ($alzPolicySetHashes.($stringHash).latestVersion -like '*-deprecated') { + $htTemp.ALZState = 'deprecated' + } + else { + $htTemp.ALZState = 'outDated' + } + } + $htTemp.ALZLatestVer = $alzPolicySetHashes.($stringHash).latestVersion + } + } + + $policySetNameMatch = $false + if ($alzPolicySets.($scopePolicySetDefinition.name) -and -not $policySetHashMatch) { + $policySetNameMatch = $true + $htTemp.ALZ = 'true' + if ($alzPolicySets.($scopePolicySetDefinition.name).metadataSource -eq 'https://github.com/Azure/Enterprise-Scale/' -and $alzPolicySets.($scopePolicySetDefinition.name).metadataSource -eq $scopePolicySetDefinition.properties.metadata.source) { + $htTemp.ALZIdentificationLevel = 'PolicySet Name, MetaData Tag' + } + else { + $htTemp.ALZIdentificationLevel = 'PolicySet Name' + } + $htTemp.ALZPolicySetName = $alzPolicySets.($scopePolicySetDefinition.name).policySetName + if ($alzPolicySets.($scopePolicySetDefinition.name).status -eq 'obsolete') { + $htTemp.ALZState = 'obsolete' + $htTemp.ALZLatestVer = '' + } + else { + if ($alzPolicySets.($scopePolicySetDefinition.name).latestVersion -eq $scopePolicySetDefinition.Properties.metadata.version) { + $htTemp.ALZState = 'upToDate' + } + else { + if ($alzPolicySets.($scopePolicySetDefinition.name).latestVersion -like '*-deprecated') { + $htTemp.ALZState = 'deprecated' + } + else { + $htTemp.ALZState = 'outDated' + } + } + $htTemp.ALZLatestVer = $alzPolicySets.($scopePolicySetDefinition.name).latestVersion + } + } + + if ($scopePolicySetDefinition.properties.metadata.source -eq 'https://github.com/Azure/Enterprise-Scale/' -and -not $policySetHashMatch -and -not $policySetNameMatch) { + $htTemp.ALZ = 'true' + $htTemp.ALZState = 'unknown' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = 'MetaData Tag' + $htTemp.ALZPolicyName = '' + $htTemp.hash = $stringHash + } + } + else { + $htTemp.ALZ = 'false' + $htTemp.ALZState = '' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = '' + $htTemp.ALZPolicySetName = '' + } + } + else { + $htTemp.ALZ = 'n/a' + $htTemp.ALZState = '' + $htTemp.ALZLatestVer = '' + $htTemp.ALZIdentificationLevel = '' + $htTemp.ALZPolicySetName = '' + } + + $htTemp.DisplayName = $($scopePolicySetDefinition.Properties.displayname) + $htTemp.Name = $scopePolicySetDefinition.Name + $htTemp.Description = $($policySetDefinitionDescription) + $htTemp.Type = $($scopePolicySetDefinition.Properties.policyType) + $htTemp.Category = $($scopePolicySetDefinition.Properties.metadata.category) + if ($scopePolicySetDefinition.Properties.metadata.version) { + $htTemp.Version = $($scopePolicySetDefinition.Properties.metadata.version) + } + else { + $htTemp.Version = 'n/a' + } + $htTemp.PolicyDefinitionId = $hlpPolicySetDefinitionId + $arrayPolicySetPolicyIdsToLower = @() + $htPolicySetPolicyRefIds = @{} + $arrayPolicySetPolicyIdsToLower = foreach ($policySetPolicy in $scopePolicySetDefinition.properties.policydefinitions) { + $($policySetPolicy.policyDefinitionId).ToLower() + $htPolicySetPolicyRefIds.($policySetPolicy.policyDefinitionReferenceId) = ($policySetPolicy.policyDefinitionId) + } + $htTemp.PolicySetPolicyIds = $arrayPolicySetPolicyIdsToLower + $htTemp.PolicySetPolicyRefIds = $htPolicySetPolicyRefIds + $htTemp.Json = $scopePolicySetDefinition + if ($scopePolicySetDefinition.Properties.metadata.deprecated -eq $true -or $scopePolicySetDefinition.Properties.displayname -like "``[Deprecated``]*") { + $htTemp.Deprecated = $scopePolicySetDefinition.Properties.metadata.deprecated + } + else { + $htTemp.Deprecated = $false + } + if ($scopePolicySetDefinition.Properties.metadata.preview -eq $true -or $scopePolicySetDefinition.Properties.displayname -like "``[*Preview``]*") { + $htTemp.Preview = $scopePolicySetDefinition.Properties.metadata.preview + } + else { + $htTemp.Preview = $false + } + ($script:htCacheDefinitionsPolicySet).($hlpPolicySetDefinitionId) = $htTemp + } + #namingValidation + if (-not [string]::IsNullOrEmpty($scopePolicySetDefinition.Properties.displayname)) { + $namingValidationResult = NamingValidation -toCheck $scopePolicySetDefinition.Properties.displayname + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id)) { + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id) = @{} + } + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id).displayNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id).displayName = $scopePolicySetDefinition.Properties.displayname + } + } + if (-not [string]::IsNullOrEmpty($scopePolicySetDefinition.Name)) { + $namingValidationResult = NamingValidation -toCheck $scopePolicySetDefinition.Name + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id)) { + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id) = @{} + } + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicySet.($scopePolicySetDefinition.Id).name = $scopePolicySetDefinition.Name + } + } + } + } + + $returnObject = @{} + $returnObject.'PolicySetDefinitionsScopedCount' = $PolicySetDefinitionsScopedCount + return $returnObject +} +$funcDataCollectionPolicySetDefinitions = $function:dataCollectionPolicySetDefinitions.ToString() + +function dataCollectionPolicyAssignmentsMG { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $mgParentId, + $mgParentName, + $mgAscSecureScoreResult, + $PolicyDefinitionsScopedCount, + $PolicySetDefinitionsScopedCount + ) + + $addRowToTableDone = $false + $currentTask = "Getting Policy assignments for Management Group: '$($scopeDisplayName)' ('$($scopeId)')" + if ($azAPICallConf['htParameters'].LargeTenant -eq $false -or $azAPICallConf['htParameters'].PolicyAtScopeOnly -eq $false) { + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($scopeId)/providers/Microsoft.Authorization/policyAssignments?`$filter=atscope()&api-version=2021-06-01" + } + else { + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementgroups/$($scopeId)/providers/Microsoft.Authorization/policyAssignments?`$filter=atExactScope()&api-version=2021-06-01" + } + $method = 'GET' + $L0mgmtGroupPolicyAssignments = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $L0mgmtGroupPolicyAssignmentsPolicyCount = (($L0mgmtGroupPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' } ))).count + $L0mgmtGroupPolicyAssignmentsPolicySetCount = (($L0mgmtGroupPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' } ))).count + $L0mgmtGroupPolicyAssignmentsPolicyAtScopeCount = (($L0mgmtGroupPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($scopeId)" } ))).count + $L0mgmtGroupPolicyAssignmentsPolicySetAtScopeCount = (($L0mgmtGroupPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -match "/providers/Microsoft.Management/managementGroups/$($scopeId)" } ))).count + $L0mgmtGroupPolicyAssignmentsPolicyAndPolicySetAtScopeCount = ($L0mgmtGroupPolicyAssignmentsPolicyAtScopeCount + $L0mgmtGroupPolicyAssignmentsPolicySetAtScopeCount) + + if (-not $htMgAtScopePolicyAssignments.($scopeId)) { + $script:htMgAtScopePolicyAssignments.($scopeId) = @{} + $script:htMgAtScopePolicyAssignments.($scopeId).AssignmentsCount = $L0mgmtGroupPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + + foreach ($L0mgmtGroupPolicyAssignment in $L0mgmtGroupPolicyAssignments) { + + $doIt = $false + if ($L0mgmtGroupPolicyAssignment.properties.scope -eq "/providers/Microsoft.Management/managementGroups/$($scopeId)" -and $L0mgmtGroupPolicyAssignment.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)") { + $doIt = $true + } + if ($scopeId -eq $ManagementGroupId) { + $doIt = $true + } + + if ($doIt) { + $htTemp = @{} + $htTemp.Assignment = $L0mgmtGroupPolicyAssignment + $htTemp.AssignmentScopeMgSubRg = 'Mg' + $splitAssignment = (($L0mgmtGroupPolicyAssignment.Id).ToLower()).Split('/') + $htTemp.AssignmentScopeId = [string]($splitAssignment[4]) + $script:htCacheAssignmentsPolicy.(($L0mgmtGroupPolicyAssignment.Id).ToLower()) = $htTemp + } + + #region namingValidation + if (-not [string]::IsNullOrEmpty($L0mgmtGroupPolicyAssignment.Properties.DisplayName)) { + $namingValidationResult = NamingValidation -toCheck $L0mgmtGroupPolicyAssignment.Properties.DisplayName + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id)) { + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id) = @{} + } + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id).displayNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id).displayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName + } + } + if (-not [string]::IsNullOrEmpty($L0mgmtGroupPolicyAssignment.Name)) { + $namingValidationResult = NamingValidation -toCheck $L0mgmtGroupPolicyAssignment.Name + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id)) { + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id) = @{} + } + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicyAssignment.($L0mgmtGroupPolicyAssignment.Id).name = $L0mgmtGroupPolicyAssignment.Name + } + } + #endregion namingValidation + + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -OR $L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + + #policy + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/') { + $policyVariant = 'Policy' + $policyDefinitionId = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() + + $policyDefinitionSplitted = $policyDefinitionId.split('/') + $hlpPolicyDefinitionScope = $policyDefinitionSplitted[4] + + if ( ($policyDefinitionId -like '/providers/microsoft.management/managementgroups/*' -and $htManagementGroupsMgPath.($scopeId).path -contains ($hlpPolicyDefinitionScope)) -or $policyDefinitionId -like '/providers/microsoft.authorization/policydefinitions/*' ) { + $tryCounter = 0 + do { + $tryCounter++ + if (($htCacheDefinitionsPolicy).($policyDefinitionId)) { + $policyReturnedFromHt = $true + $policyDefinition = ($htCacheDefinitionsPolicy).($policyDefinitionId) + + if ([string]::IsnullOrEmpty($policyDefinition.PolicyDefinitionId)) { + Write-Host "check: $policyDefinitionId" + $policyDefinition + } + + if ($policyDefinition.Type -eq 'Custom') { + $policyDefintionScope = $policyDefinition.Scope + $policyDefintionScopeMgSub = $policyDefinition.ScopeMgSub + $policyDefintionScopeId = $policyDefinition.ScopeId + } + else { + $policyDefintionScope = 'n/a' + $policyDefintionScopeMgSub = 'n/a' + $policyDefintionScopeId = 'n/a' + } + + $policyAvailability = '' + $policyDisplayName = ($policyDefinition).DisplayName + $policyDescription = ($policyDefinition).Description + $policyDefinitionType = ($policyDefinition).Type + $policyDefinitionIsALZ = ($policyDefinition).ALZ + $policyCategory = ($policyDefinition).Category + $policyDefinitionEffectDefault = ($policyDefinition).effectDefaultValue + $policyDefinitionEffectFixed = ($policyDefinition).effectFixedValue + } + else { + #test + Write-Host " attention! $scopeDisplayName ($scopeId); policyAssignment '$($L0mgmtGroupPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' -retry" + Start-Sleep -Seconds 1 + } + } + until ($policyReturnedFromHt -or $tryCounter -gt 2) + if (-not $policyReturnedFromHt) { + Write-Host " attention! $scopeDisplayName ($scopeId); policyAssignment '$($L0mgmtGroupPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)'" + Write-Host " scope: $($scopeId) Policy / Custom:$($mgPolicyDefinitions.Count) CustomAtScope:$($PolicyDefinitionsScopedCount)" + Write-Host " built-in PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'BuiltIn'}).Count)" + Write-Host " custom PolicyDefinitions: $($($htCacheDefinitionsPolicy).Values.where({$_.Type -eq 'Custom'}).Count)" + Write-Host ' Listing all PolicyDefinitions:' + foreach ($tmpPolicyDefinitionId in ($($htCacheDefinitionsPolicy).Keys | Sort-Object)) { + Write-Host $tmpPolicyDefinitionId + } + Throw 'Error - Azure Governance Visualizer: check the last console output for details' + } + } + #policyDefinition Scope does not exist + else { + if ($htManagementGroupsMgPath.Keys -contains $hlpPolicyDefinitionScope) { + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L0mgmtGroupPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' - the scope '$($hlpPolicyDefinitionScope)' is not contained in the '$scopeId' Management Group chain. The Policy definition scope '$hlpPolicyDefinitionScope' has MGPath: '$($htManagementGroupsMgPath.($hlpPolicyDefinitionScope).pathDelimited)'" + } + else { + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L0mgmtGroupPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' - the scope '$($hlpPolicyDefinitionScope)' could not be found" + } + $policyAvailability = 'na' + + $policyDefintionScope = "/$($policyDefinitionSplitted[1])/$($policyDefinitionSplitted[2])/$($policyDefinitionSplitted[3])/$($hlpPolicyDefinitionScope)" + $policyDefintionScopeMgSub = 'Mg' + $policyDefintionScopeId = $hlpPolicyDefinitionScope + + $policyDisplayName = 'unknown' + $policyDescription = 'unknown' + $policyDefinitionType = 'likely Custom' + $policyDefinitionIsALZ = 'unknown' + $policyCategory = 'unknown' + $policyDefinitionEffectDefault = 'unknown' + $policyDefinitionEffectFixed = 'unknown' + } + + $policyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope + $policyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() + $policyAssignmentName = $L0mgmtGroupPolicyAssignment.Name + $policyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName + if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { + $policyAssignmentDescription = 'no description given' + } + else { + $policyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description + } + + if ($L0mgmtGroupPolicyAssignment.identity) { + $policyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId + } + else { + $policyAssignmentIdentity = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L0mgmtGroupPolicyAssignment.properties.metadata) { + if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn + } + } + + if ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.Message) { + $nonComplianceMessage = $L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $scopeDisplayName ` + -mgId $scopeId ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Policy $policyDisplayName ` + -PolicyAvailability $policyAvailability ` + -PolicyDescription $policyDescription ` + -PolicyVariant $policyVariant ` + -PolicyType $policyDefinitionType ` + -PolicyIsALZ $policyDefinitionIsALZ ` + -PolicyCategory $policyCategory ` + -PolicyDefinitionIdGuid ($policyDefinitionId -replace '.*/') ` + -PolicyDefinitionId $policyDefinitionId ` + -PolicyDefintionScope $policyDefintionScope ` + -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` + -PolicyDefintionScopeId $policyDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` + -PolicyDefinitionsScopedCount $policyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` + -PolicySetDefinitionsScopedCount $policySetDefinitionsScopedCount ` + -PolicyDefinitionEffectDefault $policyDefinitionEffectDefault ` + -PolicyDefinitionEffectFixed $policyDefinitionEffectFixed ` + -PolicyAssignmentScope $policyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg 'Mg' ` + -PolicyAssignmentScopeName ($policyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $policyAssignmentId ` + -PolicyAssignmentName $policyAssignmentName ` + -PolicyAssignmentDisplayName $policyAssignmentDisplayName ` + -PolicyAssignmentDescription $policyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $nonComplianceMessage ` + -PolicyAssignmentIdentity $policyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` + -PolicyAssignmentCount $L0mgmtGroupPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` + -PolicySetAssignmentCount $L0mgmtGroupPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + + #policySet + if ($L0mgmtGroupPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + $policyVariant = 'PolicySet' + $policySetDefinitionId = ($L0mgmtGroupPolicyAssignment.properties.policydefinitionid).ToLower() + $policySetDefinitionSplitted = $policySetDefinitionId.split('/') + $hlpPolicySetDefinitionScope = $policySetDefinitionSplitted[4] + + $tryCounter = 0 + do { + $tryCounter++ + if (($htCacheDefinitionsPolicySet).($policySetDefinitionId)) { + $policySetReturnedFromHt = $true + $policySetDefinition = ($htCacheDefinitionsPolicySet).($policySetDefinitionId) + if ($policySetDefinition.Type -eq 'Custom') { + $policySetDefintionScope = $policySetDefinition.Scope + $policySetDefintionScopeMgSub = $policySetDefinition.ScopeMgSub + $policySetDefintionScopeId = $policySetDefinition.ScopeId + } + else { + $policySetDefintionScope = 'n/a' + $policySetDefintionScopeMgSub = 'n/a' + $policySetDefintionScopeId = 'n/a' + } + $policySetDisplayName = $policySetDefinition.DisplayName + $policySetDescription = $policySetDefinition.Description + $policySetDefinitionType = $policySetDefinition.Type + $policySetDefinitionIsALZ = $policySetDefinition.ALZ + $policySetCategory = $policySetDefinition.Category + } + else { + #test + #Write-Host "pa '($L0mgmtGroupPolicyAssignment.Id)' scope: '$($scopeId)' - policySetDefinition not available: $policySetDefinitionId" + Start-Sleep -Seconds 1 + } + } + until ($policySetReturnedFromHt -or $tryCounter -gt 2) + if (-not $policySetReturnedFromHt) { + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L0mgmtGroupPolicyAssignment.Id)' policyDefinition (PolicySet) could not be found: '$($policySetDefinitionId)'" + $policySetDefintionScope = "/$($policySetDefinitionSplitted[1])/$($policySetDefinitionSplitted[2])/$($policySetDefinitionSplitted[3])/$($hlpPolicySetDefinitionScope)" + $policySetDefintionScopeMgSub = 'Mg' + $policySetDefintionScopeId = $hlpPolicySetDefinitionScope + $policySetDisplayName = 'unknown' + $policySetDescription = 'unknown' + $policySetDefinitionType = 'likely Custom' + $policySetDefinitionIsALZ = 'unknown' + $policySetCategory = 'unknown' + } + + $policyAssignmentScope = $L0mgmtGroupPolicyAssignment.Properties.Scope + $policyAssignmentId = ($L0mgmtGroupPolicyAssignment.Id).ToLower() + $policyAssignmentName = $L0mgmtGroupPolicyAssignment.Name + $policyAssignmentDisplayName = $L0mgmtGroupPolicyAssignment.Properties.DisplayName + if (($L0mgmtGroupPolicyAssignment.Properties.Description).length -eq 0) { + $policyAssignmentDescription = 'no description given' + } + else { + $policyAssignmentDescription = $L0mgmtGroupPolicyAssignment.Properties.Description + } + + if ($L0mgmtGroupPolicyAssignment.identity) { + $policyAssignmentIdentity = $L0mgmtGroupPolicyAssignment.identity.principalId + } + else { + $policyAssignmentIdentity = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L0mgmtGroupPolicyAssignment.properties.metadata) { + if ($L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.assignedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L0mgmtGroupPolicyAssignment.properties.metadata.createdBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L0mgmtGroupPolicyAssignment.properties.metadata.createdOn + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedBy + } + if ($L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L0mgmtGroupPolicyAssignment.properties.metadata.updatedOn + } + } + + if (($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId } )).Message) { + $nonComplianceMessage = ($L0mgmtGroupPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId } )).Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L0mgmtGroupPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $scopeDisplayName ` + -mgId $scopeId ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Policy $policySetDisplayName ` + -PolicyDescription $policySetDescription ` + -PolicyVariant $policyVariant ` + -PolicyType $policySetDefinitionType ` + -PolicyIsALZ $policySetDefinitionIsALZ ` + -PolicyCategory $policySetCategory ` + -PolicyDefinitionIdGuid ($policySetDefinitionId -replace '.*/') ` + -PolicyDefinitionId $policySetDefinitionId ` + -PolicyDefintionScope $policySetDefintionScope ` + -PolicyDefintionScopeMgSub $policySetDefintionScopeMgSub ` + -PolicyDefintionScopeId $policySetDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedManagementGroup ` + -PolicyDefinitionsScopedCount $policyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedManagementGroup ` + -PolicySetDefinitionsScopedCount $policySetDefinitionsScopedCount ` + -PolicyAssignmentScope $policyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg 'Mg' ` + -PolicyAssignmentScopeName ($policyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L0mgmtGroupPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $policyAssignmentId ` + -PolicyAssignmentName $policyAssignmentName ` + -PolicyAssignmentDisplayName $policyAssignmentDisplayName ` + -PolicyAssignmentDescription $policyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L0mgmtGroupPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $nonComplianceMessage ` + -PolicyAssignmentIdentity $policyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsManagementGroup ` + -PolicyAssignmentCount $L0mgmtGroupPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L0mgmtGroupPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsManagementGroup ` + -PolicySetAssignmentCount $L0mgmtGroupPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $L0mgmtGroupPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + } + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionPolicyAssignmentsMG = $function:dataCollectionPolicyAssignmentsMG.ToString() + +function dataCollectionPolicyAssignmentsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $childMgDisplayName, + $childMgId, + $childMgParentId, + $childMgParentName, + $mgAscSecureScoreResult, + $subscriptionQuotaId, + $subscriptionState, + $subscriptionASCSecureScore, + $subscriptionTags, + $subscriptionTagsCount, + $PolicyDefinitionsScopedCount, + $PolicySetDefinitionsScopedCount + ) + + $currentTask = "Getting Policy assignments for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/policyAssignments?api-version=2021-06-01" + $method = 'GET' + + $addRowToTableDone = $false + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy -eq $false) { + $L1mgmtGroupSubPolicyAssignments = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $L1mgmtGroupSubPolicyAssignmentsPolicyCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicySetCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicyAtScopeCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -match "/subscriptions/$($scopeId)" } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicySetAtScopeCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -match "/subscriptions/$($scopeId)" } )).count + $L1mgmtGroupSubPolicyAssignmentsQuery = $L1mgmtGroupSubPolicyAssignments + } + else { + $L1mgmtGroupSubPolicyAssignments = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $L1mgmtGroupSubPolicyAssignmentsPolicyCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -notmatch "/subscriptions/$($scopeId)/resourceGroups" } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicySetCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -notmatch "/subscriptions/$($scopeId)/resourceGroups" } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicyAtScopeCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -and $_.Id -match "/subscriptions/$($scopeId)" -and $_.Id -notmatch "/subscriptions/$($scopeId)/resourceGroups" } )).count + $L1mgmtGroupSubPolicyAssignmentsPolicySetAtScopeCount = ($L1mgmtGroupSubPolicyAssignments.where( { $_.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/' -and $_.Id -match "/subscriptions/$($scopeId)" -and $_.Id -notmatch "/subscriptions/$($scopeId)/resourceGroups" } )).count + foreach ($L1mgmtGroupSubPolicyAssignment in $L1mgmtGroupSubPolicyAssignments.where( { $_.Id -match "/subscriptions/$($scopeId)/resourceGroups" } )) { + ($script:htCacheAssignmentsPolicyOnResourceGroupsAndResources).(($L1mgmtGroupSubPolicyAssignment.Id).ToLower()) = $L1mgmtGroupSubPolicyAssignment + } + $L1mgmtGroupSubPolicyAssignmentsQuery = $L1mgmtGroupSubPolicyAssignments.where( { $_.Id -notmatch "/subscriptions/$($scopeId)/resourceGroups" } ) + } + + $L1mgmtGroupSubPolicyAssignmentsPolicyAndPolicySetAtScopeCount = ($L1mgmtGroupSubPolicyAssignmentsPolicyAtScopeCount + $L1mgmtGroupSubPolicyAssignmentsPolicySetAtScopeCount) + + foreach ($L1mgmtGroupSubPolicyAssignment in $L1mgmtGroupSubPolicyAssignmentsQuery ) { + if ($L1mgmtGroupSubPolicyAssignment.Id -like "/subscriptions/$($scopeId)/*") { + $htTemp = @{} + $htTemp.Assignment = $L1mgmtGroupSubPolicyAssignment + $splitAssignment = (($L1mgmtGroupSubPolicyAssignment.Id).ToLower()).Split('/') + if (($L1mgmtGroupSubPolicyAssignment.Id).ToLower() -like "/subscriptions/$($scopeId)/resourceGroups*") { + $htTemp.AssignmentScopeMgSubRg = 'Rg' + $htTemp.AssignmentScopeId = "$($splitAssignment[2])/$($splitAssignment[4])" + } + else { + $htTemp.AssignmentScopeMgSubRg = 'Sub' + $htTemp.AssignmentScopeId = [string]$splitAssignment[2] + } + $script:htCacheAssignmentsPolicy.(($L1mgmtGroupSubPolicyAssignment.Id).ToLower()) = $htTemp + } + + #region namingValidation + if (-not [string]::IsNullOrEmpty($L1mgmtGroupSubPolicyAssignment.Properties.DisplayName)) { + $namingValidationResult = NamingValidation -toCheck $L1mgmtGroupSubPolicyAssignment.Properties.DisplayName + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id)) { + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id) = @{} + } + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id).displayNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id).displayName = $L1mgmtGroupSubPolicyAssignment.Properties.DisplayName + } + } + if (-not [string]::IsNullOrEmpty($L1mgmtGroupSubPolicyAssignment.Name)) { + $namingValidationResult = NamingValidation -toCheck $L1mgmtGroupSubPolicyAssignment.Name + if ($namingValidationResult.Count -gt 0) { + if (-not $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id)) { + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id) = @{} + } + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id).nameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.PolicyAssignment.($L1mgmtGroupSubPolicyAssignment.Id).name = $L1mgmtGroupSubPolicyAssignment.Name + } + } + #endregion namingValidation + + if ($L1mgmtGroupSubPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/' -OR $L1mgmtGroupSubPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + + #policy + if ($L1mgmtGroupSubPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policyDefinitions/') { + $policyVariant = 'Policy' + $policyDefinitionId = ($L1mgmtGroupSubPolicyAssignment.properties.policydefinitionid).ToLower() + + if (($htCacheDefinitionsPolicy).($policyDefinitionId)) { + $policyAvailability = '' + + #handling some strange scenario where the synchronized hashTable responds fragments?! + $tryCounter = 0 + do { + $tryCounter++ + $policyAssignmentsPolicyDefinition = ($htCacheDefinitionsPolicy).($policyDefinitionId) + + if (($policyAssignmentsPolicyDefinition).Type -eq 'Custom' -or ($policyAssignmentsPolicyDefinition).Type -eq 'Builtin') { + $policyReturnedFromHt = $true + + $policyDisplayName = ($policyAssignmentsPolicyDefinition).DisplayName + $policyDescription = ($policyAssignmentsPolicyDefinition).Description + $policyDefinitionType = ($policyAssignmentsPolicyDefinition).Type + $policyDefinitionIsALZ = ($policyAssignmentsPolicyDefinition).ALZ + $policyCategory = ($policyAssignmentsPolicyDefinition).Category + $policyDefinitionEffectDefault = ($policyAssignmentsPolicyDefinition).effectDefaultValue + $policyDefinitionEffectFixed = ($policyAssignmentsPolicyDefinition).effectFixedValue + + if (($policyAssignmentsPolicyDefinition).Type -ne $policyDefinitionType) { + Write-Host "$scopeDisplayName ($scopeId) $policyVariant was processing: $policyDefinitionId" + Write-Host "'$(($policyAssignmentsPolicyDefinition).Type)' ne '$policyDefinitionType'" + Write-Host "!Please report this error: $($azAPICallConf['htParameters'].GithubRepository)" -ForegroundColor Yellow + throw + } + + if ($policyDefinitionType -eq 'Custom') { + $policyDefintionScope = ($policyAssignmentsPolicyDefinition).Scope + $policyDefintionScopeMgSub = ($policyAssignmentsPolicyDefinition).ScopeMgSub + $policyDefintionScopeId = ($policyAssignmentsPolicyDefinition).ScopeId + } + + if ($policyDefinitionType -eq 'Builtin') { + $policyDefintionScope = 'n/a' + $policyDefintionScopeMgSub = 'n/a' + $policyDefintionScopeId = 'n/a' + } + } + else { + Write-Host " **INCONSISTENCY! processing policyId:'$policyDefinitionId'; policyAss:'$($L1mgmtGroupSubPolicyAssignment.Id)'; policyAssignmentsPolicyDefinition.Type: '$($policyAssignmentsPolicyDefinition.Type)'" + Start-Sleep -Seconds 1 + } + } + until($policyReturnedFromHt -or $tryCounter -gt 5) + if (-not $policyReturnedFromHt) { + Write-Host "FinalHandler - $scopeDisplayName ($scopeId) $policyVariant was processing: policyId:'$policyDefinitionId'; policyAss:'$($L1mgmtGroupSubPolicyAssignment.Id)'; policyAssignmentsPolicyDefinition.Type: '$($policyAssignmentsPolicyDefinition.Type)'" + Write-Host ($policyAssignmentsPolicyDefinition | ConvertTo-Json -Depth 99) + Write-Host "!Please report this error: $($azAPICallConf['htParameters'].GithubRepository)" -ForegroundColor Yellow + throw + } + } + #policyDefinition not exists! + else { + $policyDefinitionSplitted = $policyDefinitionId.split('/') + + if ($policyDefinitionId -like '/providers/microsoft.management/managementgroups/*') { + $hlpPolicyDefinitionScope = $policyDefinitionSplitted[4] + if ($htSubscriptionsMgPath.($scopeId).path -contains $hlpPolicyDefinitionScope) { + Write-Host " ATTENTION: $scopeDisplayName ($scopeId); policyAssignment '$($L1mgmtGroupSubPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' - the scope '$($hlpPolicyDefinitionScope)' HOWEVER IS CONTAINED in the '$scopeId' Management Group chain. The Policy definition scope '$hlpPolicyDefinitionScope' has MGPath: '$($htManagementGroupsMgPath.($hlpPolicyDefinitionScope).pathDelimited)'" + } + else { + if ($htManagementGroupsMgPath.($hlpPolicyDefinitionScope)) { + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L1mgmtGroupSubPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' - the scope '$($hlpPolicyDefinitionScope)' IS NOT CONTAINED in the '$scopeId' Management Group chain. The Policy definition scope '$hlpPolicyDefinitionScope' has MGPath: '$($htManagementGroupsMgPath.($hlpPolicyDefinitionScope).pathDelimited)'" + } + else { + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L1mgmtGroupSubPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)' - the scope '$($hlpPolicyDefinitionScope)' IS NOT CONTAINED in the '$scopeId' Management Group chain. The Policy definition scope '$hlpPolicyDefinitionScope' could not be found" + } + } + $policyDefintionScope = "/$($policyDefinitionSplitted[1])/$($policyDefinitionSplitted[2])/$($policyDefinitionSplitted[3])/$($hlpPolicyDefinitionScope)" + $policyDefintionScopeMgSub = 'Mg' + $policyDefintionScopeId = $hlpPolicyDefinitionScope + } + else { + $hlpPolicyDefinitionScope = $policyDefinitionSplitted[2] + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L1mgmtGroupSubPolicyAssignment.Id)' policyDefinition (Policy) could not be found: '$($policyDefinitionId)'" + $policyDefintionScope = "/$($policyDefinitionSplitted[1])/$($hlpPolicyDefinitionScope)" + $policyDefintionScopeMgSub = 'Sub' + $policyDefintionScopeId = $hlpPolicyDefinitionScope + } + $policyAvailability = 'na' + $policyDisplayName = 'unknown' + $policyDescription = 'unknown' + $policyDefinitionType = 'likely Custom' + $policyDefinitionIsALZ = 'unknown' + $policyCategory = 'unknown' + $policyDefinitionEffectDefault = 'unknown' + $policyDefinitionEffectFixed = 'unknown' + } + + $PolicyAssignmentScope = $L1mgmtGroupSubPolicyAssignment.Properties.Scope + if ($PolicyAssignmentScope -like '/providers/Microsoft.Management/managementGroups/*') { + $PolicyAssignmentScopeMgSubRg = 'Mg' + } + else { + $splitPolicyAssignmentScope = ($PolicyAssignmentScope).Split('/') + switch (($splitPolicyAssignmentScope).Count - 1) { + #sub + 2 { + $PolicyAssignmentScopeMgSubRg = 'Sub' + } + 4 { + $PolicyAssignmentScopeMgSubRg = 'Rg' + } + Default { + $PolicyAssignmentScopeMgSubRg = 'unknown' + } + } + } + + $PolicyAssignmentId = ($L1mgmtGroupSubPolicyAssignment.Id).ToLower() + $PolicyAssignmentName = $L1mgmtGroupSubPolicyAssignment.Name + $PolicyAssignmentDisplayName = $L1mgmtGroupSubPolicyAssignment.Properties.DisplayName + if (($L1mgmtGroupSubPolicyAssignment.Properties.Description).length -eq 0) { + $PolicyAssignmentDescription = 'no description given' + } + else { + $PolicyAssignmentDescription = $L1mgmtGroupSubPolicyAssignment.Properties.Description + } + + if ($L1mgmtGroupSubPolicyAssignment.identity) { + $PolicyAssignmentIdentity = $L1mgmtGroupSubPolicyAssignment.identity.principalId + } + else { + $PolicyAssignmentIdentity = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata) { + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.assignedBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.createdBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L1mgmtGroupSubPolicyAssignment.properties.metadata.createdOn + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedOn + } + } + + if ($L1mgmtGroupSubPolicyAssignment.Properties.nonComplianceMessages.Message) { + $nonComplianceMessage = $L1mgmtGroupSubPolicyAssignment.Properties.nonComplianceMessages.Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L1mgmtGroupSubPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $scopeDisplayName ` + -SubscriptionId $scopeId ` + -SubscriptionQuotaId $subscriptionQuotaId ` + -SubscriptionState $subscriptionState ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore ` + -SubscriptionTags $subscriptionTags ` + -SubscriptionTagsCount $subscriptionTagsCount ` + -Policy $policyDisplayName ` + -PolicyAvailability $policyAvailability ` + -PolicyDescription $policyDescription ` + -PolicyVariant $policyVariant ` + -PolicyType $policyDefinitionType ` + -PolicyIsALZ $policyDefinitionIsALZ ` + -PolicyCategory $policyCategory ` + -PolicyDefinitionIdGuid ($policyDefinitionId -replace '.*/') ` + -PolicyDefinitionId $policyDefinitionId ` + -PolicyDefintionScope $policyDefintionScope ` + -PolicyDefintionScopeMgSub $policyDefintionScopeMgSub ` + -PolicyDefintionScopeId $policyDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedSubscription ` + -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedSubscription ` + -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` + -PolicyDefinitionEffectDefault $policyDefinitionEffectDefault ` + -PolicyDefinitionEffectFixed $policyDefinitionEffectFixed ` + -PolicyAssignmentScope $PolicyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg $PolicyAssignmentScopeMgSubRg ` + -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L1mgmtGroupSubPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $PolicyAssignmentId ` + -PolicyAssignmentName $PolicyAssignmentName ` + -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` + -PolicyAssignmentDescription $PolicyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L1mgmtGroupSubPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $nonComplianceMessage ` + -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsSubscription ` + -PolicyAssignmentCount $L1mgmtGroupSubPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L1mgmtGroupSubPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsSubscription ` + -PolicySetAssignmentCount $L1mgmtGroupSubPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + + #policySet + if ($L1mgmtGroupSubPolicyAssignment.properties.policyDefinitionId -match '/providers/Microsoft.Authorization/policySetDefinitions/') { + $policyVariant = 'PolicySet' + $policySetDefinitionId = ($L1mgmtGroupSubPolicyAssignment.properties.policydefinitionid).ToLower() + $policySetDefinitionSplitted = $policySetDefinitionId.split('/') + + if (($htCacheDefinitionsPolicySet).($policySetDefinitionId)) { + $policyAvailability = '' + + #handling some strange behavior where the synchronized hashTable responds fragments?! + $tryCounter = 0 + do { + $tryCounter++ + $policyAssignmentsPolicySetDefinition = ($htCacheDefinitionsPolicySet).($policySetDefinitionId) + + if (($policyAssignmentsPolicySetDefinition).Type -eq 'Custom' -or ($policyAssignmentsPolicySetDefinition).Type -eq 'Builtin') { + $policySetReturnedFromHt = $true + + $policySetDisplayName = ($policyAssignmentsPolicySetDefinition).DisplayName + $policySetDescription = ($policyAssignmentsPolicySetDefinition).Description + $policySetDefinitionType = ($policyAssignmentsPolicySetDefinition).Type + $policySetDefinitionIsALZ = ($policyAssignmentsPolicySetDefinition).ALZ + $policySetCategory = ($policyAssignmentsPolicySetDefinition).Category + + if (($policyAssignmentsPolicySetDefinition).Type -ne $policySetDefinitionType) { + Write-Host "$scopeDisplayName ($scopeId) $policyVariant was processing: $policySetDefinitionId" + Write-Host "'$(($policyAssignmentsPolicySetDefinition).Type)' ne '$policySetDefinitionType'" + Write-Host "!Please report this error: $($azAPICallConf['htParameters'].GithubRepository)" -ForegroundColor Yellow + throw + } + + if ($policySetDefinitionType -eq 'Custom') { + $policySetDefintionScope = ($policyAssignmentsPolicySetDefinition).Scope + $policySetDefintionScopeMgSub = ($policyAssignmentsPolicySetDefinition).ScopeMgSub + $policySetDefintionScopeId = ($policyAssignmentsPolicySetDefinition).ScopeId + } + if ($policySetDefinitionType -eq 'Builtin') { + $policySetDefintionScope = 'n/a' + $policySetDefintionScopeMgSub = 'n/a' + $policySetDefintionScopeId = 'n/a' + } + } + else { + #Write-Host "TryHandler - $scopeDisplayName ($scopeId) $policyVariant was processing: policySetId:'$policySetDefinitionId'; policyAss:'$($L1mgmtGroupSubPolicyAssignment.Id)'; type:'$(($policyAssignmentsPolicySetDefinition).Type)' - sleeping '$tryCounter' seconds" + Start-Sleep -Seconds 1 + } + } + until($policySetReturnedFromHt -or $tryCounter -gt 5) + if (-not $policySetReturnedFromHt) { + Write-Host "FinalHandler - $scopeDisplayName ($scopeId) $policyVariant was processing: policySetId:'$policySetDefinitionId'; policyAss:'$($L1mgmtGroupSubPolicyAssignment.Id)'" + Write-Host "!Please report this error: $($azAPICallConf['htParameters'].GithubRepository)" -ForegroundColor Yellow + throw + } + } + #policySetDefinition not exists! + else { + $policyAvailability = 'na' + $policySetDisplayName = 'unknown' + $policySetDescription = 'unknown' + $policySetDefinitionType = 'likely Custom' + $policySetDefinitionIsALZ = 'unknown' + $policySetCategory = 'unknown' + + if ($policySetDefinitionId -like '/providers/microsoft.management/managementgroups/*') { + $hlpPolicySetDefinitionScope = $policySetDefinitionSplitted[4] + $policySetDefintionScope = "/$($policySetDefinitionSplitted[1])/$($policySetDefinitionSplitted[2])/$($policySetDefinitionSplitted[3])/$($hlpPolicySetDefinitionScope)" + $policySetDefintionScopeMgSub = 'Mg' + $policySetDefintionScopeId = $hlpPolicySetDefinitionScope + } + else { + $hlpPolicySetDefinitionScope = $policySetDefinitionSplitted[2] + $policySetDefintionScope = "/$($policySetDefinitionSplitted[1])/$($hlpPolicySetDefinitionScope)" + $policySetDefintionScopeMgSub = 'Sub' + $policySetDefintionScopeId = $hlpPolicySetDefinitionScope + + } + Write-Host " $scopeDisplayName ($scopeId); policyAssignment '$($L1mgmtGroupSubPolicyAssignment.Id)' policyDefinition (PolicySet) could not be found: '$($policySetDefinitionId)'" + } + + $PolicyAssignmentScope = $L1mgmtGroupSubPolicyAssignment.Properties.Scope + if ($PolicyAssignmentScope -like '/providers/Microsoft.Management/managementGroups/*') { + $PolicyAssignmentScopeMgSubRg = 'Mg' + } + else { + $splitPolicyAssignmentScope = ($PolicyAssignmentScope).Split('/') + switch (($splitPolicyAssignmentScope).Count - 1) { + #sub + 2 { + $PolicyAssignmentScopeMgSubRg = 'Sub' + } + 4 { + $PolicyAssignmentScopeMgSubRg = 'Rg' + } + Default { + $PolicyAssignmentScopeMgSubRg = 'unknown' + } + } + } + + $PolicyAssignmentId = ($L1mgmtGroupSubPolicyAssignment.Id).ToLower() + $PolicyAssignmentName = $L1mgmtGroupSubPolicyAssignment.Name + $PolicyAssignmentDisplayName = $L1mgmtGroupSubPolicyAssignment.Properties.DisplayName + if (($L1mgmtGroupSubPolicyAssignment.Properties.Description).length -eq 0) { + $PolicyAssignmentDescription = 'no description given' + } + else { + $PolicyAssignmentDescription = $L1mgmtGroupSubPolicyAssignment.Properties.Description + } + + if ($L1mgmtGroupSubPolicyAssignment.identity) { + $PolicyAssignmentIdentity = $L1mgmtGroupSubPolicyAssignment.identity.principalId + } + else { + $PolicyAssignmentIdentity = 'n/a' + } + + $assignedBy = 'n/a' + $createdBy = '' + $createdOn = '' + $updatedBy = '' + $updatedOn = '' + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata) { + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.assignedBy) { + $assignedBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.assignedBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.createdBy) { + $createdBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.createdBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.createdOn) { + $createdOn = $L1mgmtGroupSubPolicyAssignment.properties.metadata.createdOn + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedBy) { + $updatedBy = $L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedBy + } + if ($L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedOn) { + $updatedOn = $L1mgmtGroupSubPolicyAssignment.properties.metadata.updatedOn + } + } + + if (($L1mgmtGroupSubPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message) { + $nonComplianceMessage = ($L1mgmtGroupSubPolicyAssignment.Properties.nonComplianceMessages.where( { -not $_.policyDefinitionReferenceId })).Message + } + else { + $nonComplianceMessage = '' + } + + $formatedPolicyAssignmentParameters = '' + $hlp = $L1mgmtGroupSubPolicyAssignment.Properties.Parameters + if (-not [string]::IsNullOrEmpty($hlp)) { + $arrayPolicyAssignmentParameters = @() + $arrayPolicyAssignmentParameters = foreach ($parameterName in $hlp.PSObject.Properties.Name | Sort-Object) { + "$($parameterName)=$($hlp.($parameterName).Value -join "$($CsvDelimiter) ")" + } + $formatedPolicyAssignmentParameters = $arrayPolicyAssignmentParameters -join "$($CsvDelimiterOpposite) " + } + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $scopeDisplayName ` + -SubscriptionId $scopeId ` + -SubscriptionQuotaId $subscriptionQuotaId ` + -SubscriptionState $subscriptionState ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore ` + -SubscriptionTags $subscriptionTags ` + -SubscriptionTagsCount $subscriptionTagsCount ` + -Policy $policySetDisplayName ` + -PolicyAvailability $policyAvailability ` + -PolicyDescription $policySetDescription ` + -PolicyVariant $policyVariant ` + -PolicyType $policySetDefinitionType ` + -PolicyIsALZ $policySetDefinitionIsALZ ` + -PolicyCategory $policySetCategory ` + -PolicyDefinitionIdGuid (($policySetDefinitionId) -replace '.*/') ` + -PolicyDefinitionId $policySetDefinitionId ` + -PolicyDefintionScope $policySetDefintionScope ` + -PolicyDefintionScopeMgSub $policySetDefintionScopeMgSub ` + -PolicyDefintionScopeId $policySetDefintionScopeId ` + -PolicyDefinitionsScopedLimit $LimitPOLICYPolicyDefinitionsScopedSubscription ` + -PolicyDefinitionsScopedCount $PolicyDefinitionsScopedCount ` + -PolicySetDefinitionsScopedLimit $LimitPOLICYPolicySetDefinitionsScopedSubscription ` + -PolicySetDefinitionsScopedCount $PolicySetDefinitionsScopedCount ` + -PolicyAssignmentScope $PolicyAssignmentScope ` + -PolicyAssignmentScopeMgSubRg $PolicyAssignmentScopeMgSubRg ` + -PolicyAssignmentScopeName ($PolicyAssignmentScope -replace '.*/', '') ` + -PolicyAssignmentNotScopes $L1mgmtGroupSubPolicyAssignment.Properties.NotScopes ` + -PolicyAssignmentId $PolicyAssignmentId ` + -PolicyAssignmentName $PolicyAssignmentName ` + -PolicyAssignmentDisplayName $PolicyAssignmentDisplayName ` + -PolicyAssignmentDescription $PolicyAssignmentDescription ` + -PolicyAssignmentEnforcementMode $L1mgmtGroupSubPolicyAssignment.Properties.EnforcementMode ` + -PolicyAssignmentNonComplianceMessages $nonComplianceMessage ` + -PolicyAssignmentIdentity $PolicyAssignmentIdentity ` + -PolicyAssignmentLimit $LimitPOLICYPolicyAssignmentsSubscription ` + -PolicyAssignmentCount $L1mgmtGroupSubPolicyAssignmentsPolicyCount ` + -PolicyAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicyAtScopeCount ` + -PolicyAssignmentParameters $L1mgmtGroupSubPolicyAssignment.Properties.Parameters ` + -PolicyAssignmentParametersFormated $formatedPolicyAssignmentParameters ` + -PolicyAssignmentAssignedBy $assignedBy ` + -PolicyAssignmentCreatedBy $createdBy ` + -PolicyAssignmentCreatedOn $createdOn ` + -PolicyAssignmentUpdatedBy $updatedBy ` + -PolicyAssignmentUpdatedOn $updatedOn ` + -PolicySetAssignmentLimit $LimitPOLICYPolicySetAssignmentsSubscription ` + -PolicySetAssignmentCount $L1mgmtGroupSubPolicyAssignmentsPolicySetCount ` + -PolicySetAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicySetAtScopeCount ` + -PolicyAndPolicySetAssignmentAtScopeCount $L1mgmtGroupSubPolicyAssignmentsPolicyAndPolicySetAtScopeCount + } + } + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionPolicyAssignmentsSub = $function:dataCollectionPolicyAssignmentsSub.ToString() + +function dataCollectionRoleDefinitions { + [CmdletBinding()]Param( + [string]$TargetMgOrSub, + [string]$scopeId, + [string]$scopeDisplayName, + $subscriptionQuotaId + ) + + if ($TargetMgOrSub -eq 'Sub') { + $currentTask = "Getting Custom Role definitions for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + } + if ($TargetMgOrSub -eq 'MG') { + $currentTask = "Getting Custom Role definitions for Management Group: '$($scopeDisplayName)' ('$scopeId')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleDefinitions?api-version=2018-07-01&`$filter=type eq 'CustomRole'" + } + $method = 'GET' + $scopeCustomRoleDefinitions = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + foreach ($scopeCustomRoleDefinition in $scopeCustomRoleDefinitions) { + if (-not $($htCacheDefinitionsRole).($scopeCustomRoleDefinition.name)) { + + if ( + ( + $scopeCustomRoleDefinition.properties.permissions.Actions -contains 'Microsoft.Authorization/roleassignments/write' -or + $scopeCustomRoleDefinition.properties.permissions.Actions -contains 'Microsoft.Authorization/roleassignments/*' -or + $scopeCustomRoleDefinition.properties.permissions.Actions -contains 'Microsoft.Authorization/*/write' -or + $scopeCustomRoleDefinition.properties.permissions.Actions -contains 'Microsoft.Authorization/*' -or + $scopeCustomRoleDefinition.properties.permissions.Actions -contains '*/write' -or + $scopeCustomRoleDefinition.properties.permissions.Actions -contains '*' + ) -and ( + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains 'Microsoft.Authorization/roleassignments/write' -and + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains 'Microsoft.Authorization/roleassignments/*' -and + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains 'Microsoft.Authorization/*/write' -and + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains 'Microsoft.Authorization/*' -and + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains '*/write' -and + $scopeCustomRoleDefinition.properties.permissions.NotActions -notcontains '*' + ) + ) { + $roleCapable4RoleAssignmentsWrite = $true + } + else { + $roleCapable4RoleAssignmentsWrite = $false + } + + $htTemp = @{} + $htTemp.Id = $($scopeCustomRoleDefinition.name) + $htTemp.Name = $($scopeCustomRoleDefinition.properties.roleName) + $htTemp.IsCustom = $true + $htTemp.AssignableScopes = $($scopeCustomRoleDefinition.properties.AssignableScopes) + $htTemp.Actions = $($scopeCustomRoleDefinition.properties.permissions.Actions) + $htTemp.NotActions = $($scopeCustomRoleDefinition.properties.permissions.NotActions) + $htTemp.DataActions = $($scopeCustomRoleDefinition.properties.permissions.DataActions) + $htTemp.NotDataActions = $($scopeCustomRoleDefinition.properties.permissions.NotDataActions) + $htTemp.Json = $scopeCustomRoleDefinition + $htTemp.RoleCanDoRoleAssignments = $roleCapable4RoleAssignmentsWrite + ($script:htCacheDefinitionsRole).($scopeCustomRoleDefinition.name) = $htTemp + + #namingValidation + if (-not [string]::IsNullOrEmpty($scopeCustomRoleDefinition.properties.roleName)) { + $namingValidationResult = NamingValidation -toCheck $scopeCustomRoleDefinition.properties.roleName + if ($namingValidationResult.Count -gt 0) { + $script:htNamingValidation.Role.($scopeCustomRoleDefinition.name) = @{} + $script:htNamingValidation.Role.($scopeCustomRoleDefinition.name).roleNameInvalidChars = ($namingValidationResult -join '') + $script:htNamingValidation.Role.($scopeCustomRoleDefinition.name).roleName = $scopeCustomRoleDefinition.properties.roleName + } + } + } + } +} +$funcDataCollectionRoleDefinitions = $function:dataCollectionRoleDefinitions.ToString() + +function dataCollectionRoleAssignmentsMG { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $mgParentId, + $mgParentName, + $mgAscSecureScoreResult + ) + + $addRowToTableDone = $false + #PIM MGRoleAssignmentScheduleInstances + if ($htDoARMRoleAssignmentScheduleInstances.Do -eq $true) { + $currentTask = "Getting ARM RoleAssignment ScheduleInstances for Management Group: '$($scopeDisplayName)' ('$($scopeId)')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleAssignmentScheduleInstances?api-version=2020-10-01" + $method = 'GET' + $roleAssignmentScheduleInstancesFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($roleAssignmentScheduleInstancesFromAPI -eq 'RoleAssignmentScheduleInstancesError' -or $roleAssignmentScheduleInstancesFromAPI -eq 'AadPremiumLicenseRequired') { + if ($roleAssignmentScheduleInstancesFromAPI -eq 'AadPremiumLicenseRequired') { + Write-Host " -> Setting 'htDoARMRoleAssignmentScheduleInstances.Do' to false (AadPremiumLicenseRequired)" + $script:htDoARMRoleAssignmentScheduleInstances.Do = $false + } + } + else { + $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) + $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count + if ($roleAssignmentScheduleInstancesCount -gt 0) { + #$htRoleAssignmentsPIM = @{} + foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { + $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties + } + } + } + } + + #RoleAssignment API MG + $currentTask = "Getting Role assignments API for Management Group: '$($scopeDisplayName)' ('$($scopeId)')" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers/Microsoft.Management/managementGroups/$($scopeId)/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" + $method = 'GET' + $roleAssignmentsFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($roleAssignmentsFromAPI.Count -gt 0) { + $principalsToResolve = @() + $principalsToResolve = foreach ($ra in $roleAssignmentsFromAPI.properties | Sort-Object -Property principalId -Unique) { + if (-not $htPrincipals.($ra.principalId)) { + $ra.principalId + } + } + + if ($principalsToResolve.Count -gt 0) { + ResolveObjectIds -objectIds $principalsToResolve + } + } + + $L0mgmtGroupRoleAssignments = $roleAssignmentsFromAPI + + $L0mgmtGroupRoleAssignmentsLimitUtilization = (($L0mgmtGroupRoleAssignments.properties.where( { $_.scope -eq "/providers/Microsoft.Management/managementGroups/$($scopeId)" } ))).count + if (-not $htMgAtScopeRoleAssignments.($scopeId)) { + $script:htMgAtScopeRoleAssignments.($scopeId) = @{} + $script:htMgAtScopeRoleAssignments.($scopeId).AssignmentsCount = $L0mgmtGroupRoleAssignmentsLimitUtilization + } + + if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $true) { + $L0mgmtGroupRoleAssignments = $L0mgmtGroupRoleAssignments.where( { $_.properties.scope -eq "/providers/Microsoft.Management/managementGroups/$($scopeId)" } ) + } + else { + #tenantLevelRoleAssignments + if (-not $htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments') { + $tenantLevelRoleAssignmentsCount = (($L0mgmtGroupRoleAssignments.where( { $_.id -like '/providers/Microsoft.Authorization/roleAssignments/*' } ))).count + $script:htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments' = @{} + $script:htMgAtScopeRoleAssignments.'tenantLevelRoleAssignments'.AssignmentsCount = $tenantLevelRoleAssignmentsCount + } + } + foreach ($L0mgmtGroupRoleAssignment in $L0mgmtGroupRoleAssignments) { + $roleAssignmentId = ($L0mgmtGroupRoleAssignment.id).ToLower() + + if ($htRoleAssignmentsPIM.($roleAssignmentId)) { + $hlperPim = $htRoleAssignmentsPIM.($roleAssignmentId) + $pim = 'true' + $pimAssignmentType = $hlperPim.assignmentType + $pimSlotStart = $($hlperPim.startDateTime) + if ($hlperPim.endDateTime) { + $pimSlotEnd = $($hlperPim.endDateTime) + } + else { + $pimSlotEnd = 'eternity' + } + } + else { + $pim = 'false' + $pimAssignmentType = '' + $pimSlotStart = '' + $pimSlotEnd = '' + } + + if (-not $htRoleAssignmentsFromAPIInheritancePrevention.($roleAssignmentId -replace '.*/')) { + $script:htRoleAssignmentsFromAPIInheritancePrevention.($roleAssignmentId -replace '.*/') = @{} + $script:htRoleAssignmentsFromAPIInheritancePrevention.($roleAssignmentId -replace '.*/').assignment = $L0mgmtGroupRoleAssignment + } + + $roleDefinitionId = $L0mgmtGroupRoleAssignment.properties.roleDefinitionId + $roleDefinitionIdGuid = $roleDefinitionId -replace '.*/' + + if (-not ($htCacheDefinitionsRole).($roleDefinitionIdGuid)) { + $roleAssignmentsRoleDefinition = '' + $roleDefinitionName = "'This roleDefinition likely was deleted although a roleAssignment existed'" + } + else { + $roleAssignmentsRoleDefinition = ($htCacheDefinitionsRole).($roleDefinitionIdGuid) + $roleDefinitionName = $roleAssignmentsRoleDefinition.Name + } + + $doIt = $false + if ($L0mgmtGroupRoleAssignment.properties.scope -eq "/providers/Microsoft.Management/managementGroups/$($scopeId)" -and $L0mgmtGroupRoleAssignment.properties.scope -ne "/providers/Microsoft.Management/managementGroups/$($ManagementGroupId)") { + $doIt = $true + } + if ($scopeId -eq $ManagementGroupId) { + $doIt = $true + } + + if ($doIt) { + #assignment + $splitAssignment = ($roleAssignmentId).Split('/') + $arrayRoleAssignment = [System.Collections.ArrayList]@() + $null = $arrayRoleAssignment.Add([PSCustomObject]@{ + RoleAssignmentId = $roleAssignmentId + Scope = $L0mgmtGroupRoleAssignment.properties.scope + DisplayName = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).displayName + SignInName = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).signInName + RoleDefinitionName = $roleDefinitionName + RoleDefinitionId = $L0mgmtGroupRoleAssignment.properties.roleDefinitionId -replace '.*/' + ObjectId = $L0mgmtGroupRoleAssignment.properties.principalId + ObjectType = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).type + PIM = $pim + }) + + $htTemp = @{} + $htTemp.Assignment = $arrayRoleAssignment + + if ($roleAssignmentId -like '/providers/Microsoft.Authorization/roleAssignments/*') { + $htTemp.AssignmentScopeTenMgSubRgRes = 'Tenant' + $htTemp.AssignmentScopeId = 'Tenant' + } + else { + $htTemp.AssignmentScopeTenMgSubRgRes = 'Mg' + $htTemp.AssignmentScopeId = [string]$splitAssignment[4] + } + ($script:htCacheAssignmentsRole).($roleAssignmentId) = $htTemp + } + + if (($htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).displayName).length -eq 0) { + $roleAssignmentIdentityDisplayname = 'n/a' + } + else { + if ($htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentityDisplayname = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).displayName + } + else { + $roleAssignmentIdentityDisplayname = 'scrubbed' + } + } + else { + $roleAssignmentIdentityDisplayname = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).displayName + } + } + if (-not $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).signInName) { + $roleAssignmentIdentitySignInName = 'n/a' + } + else { + if ($htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentitySignInName = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).signInName + } + else { + $roleAssignmentIdentitySignInName = 'scrubbed' + } + } + else { + $roleAssignmentIdentitySignInName = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).signInName + } + } + $roleAssignmentIdentityObjectId = $L0mgmtGroupRoleAssignment.properties.principalId + $roleAssignmentIdentityObjectType = $htPrincipals.($L0mgmtGroupRoleAssignment.properties.principalId).type + $roleAssignmentScope = $L0mgmtGroupRoleAssignment.properties.scope + $roleAssignmentScopeName = $roleAssignmentScope -replace '.*/' + $roleAssignmentScopeType = 'MG' + + $roleSecurityCustomRoleOwner = 0 + if ($roleAssignmentsRoleDefinition.Actions -eq '*' -and (($roleAssignmentsRoleDefinition.NotActions)).length -eq 0 -and $roleAssignmentsRoleDefinition.IsCustom -eq $True) { + $roleSecurityCustomRoleOwner = 1 + } + $roleSecurityOwnerAssignmentSP = 0 + if (($roleAssignmentsRoleDefinition.Id -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal') -or ($roleAssignmentsRoleDefinition.Actions -eq '*' -and (($roleAssignmentsRoleDefinition.NotActions)).length -eq 0 -and $roleAssignmentsRoleDefinition.IsCustom -eq $True -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal')) { + $roleSecurityOwnerAssignmentSP = 1 + } + + $createdBy = '' + $createdOn = '' + $createdOnUnformatted = $null + $updatedBy = '' + $updatedOn = '' + + if ($L0mgmtGroupRoleAssignment.properties.createdBy) { + $createdBy = $L0mgmtGroupRoleAssignment.properties.createdBy + } + if ($L0mgmtGroupRoleAssignment.properties.createdOn) { + $createdOn = $L0mgmtGroupRoleAssignment.properties.createdOn + } + if ($L0mgmtGroupRoleAssignment.properties.updatedBy) { + $updatedBy = $L0mgmtGroupRoleAssignment.properties.updatedBy + } + if ($L0mgmtGroupRoleAssignment.properties.updatedOn) { + $updatedOn = $L0mgmtGroupRoleAssignment.properties.updatedOn + } + $createdOnUnformatted = $L0mgmtGroupRoleAssignment.properties.createdOn + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $scopeDisplayName ` + -mgId $scopeId ` + -mgParentId $mgParentId ` + -mgParentName $mgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -RoleDefinitionId $roleDefinitionIdGuid ` + -RoleDefinitionName $roleDefinitionName ` + -RoleIsCustom $roleAssignmentsRoleDefinition.IsCustom ` + -RoleAssignableScopes ($roleAssignmentsRoleDefinition.AssignableScopes -join "$CsvDelimiterOpposite ") ` + -RoleActions ($roleAssignmentsRoleDefinition.Actions -join "$CsvDelimiterOpposite ") ` + -RoleNotActions ($roleAssignmentsRoleDefinition.NotActions -join "$CsvDelimiterOpposite ") ` + -RoleDataActions ($roleAssignmentsRoleDefinition.DataActions -join "$CsvDelimiterOpposite ") ` + -RoleNotDataActions ($roleAssignmentsRoleDefinition.NotDataActions -join "$CsvDelimiterOpposite ") ` + -RoleCanDoRoleAssignments $roleAssignmentsRoleDefinition.RoleCanDoRoleAssignments ` + -RoleAssignmentIdentityDisplayname $roleAssignmentIdentityDisplayname ` + -RoleAssignmentIdentitySignInName $roleAssignmentIdentitySignInName ` + -RoleAssignmentIdentityObjectId $roleAssignmentIdentityObjectId ` + -RoleAssignmentIdentityObjectType $roleAssignmentIdentityObjectType ` + -RoleAssignmentId $roleAssignmentId ` + -RoleAssignmentScope $roleAssignmentScope ` + -RoleAssignmentScopeName $roleAssignmentScopeName ` + -RoleAssignmentScopeType $roleAssignmentScopeType ` + -RoleAssignmentCreatedBy $createdBy ` + -RoleAssignmentCreatedOn $createdOn ` + -RoleAssignmentCreatedOnUnformatted $createdOnUnformatted ` + -RoleAssignmentUpdatedBy $updatedBy ` + -RoleAssignmentUpdatedOn $updatedOn ` + -RoleAssignmentsLimit $LimitRBACRoleAssignmentsManagementGroup ` + -RoleAssignmentsCount $L0mgmtGroupRoleAssignmentsLimitUtilization ` + -RoleSecurityCustomRoleOwner $roleSecurityCustomRoleOwner ` + -RoleSecurityOwnerAssignmentSP $roleSecurityOwnerAssignmentSP ` + -RoleAssignmentPIM $pim ` + -RoleAssignmentPIMAssignmentType $pimAssignmentType ` + -RoleAssignmentPIMSlotStart $pimSlotStart ` + -RoleAssignmentPIMSlotEnd $pimSlotEnd + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionRoleAssignmentsMG = $function:dataCollectionRoleAssignmentsMG.ToString() + +function dataCollectionRoleAssignmentsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + $hierarchyLevel, + $childMgDisplayName, + $childMgId, + $childMgParentId, + $childMgParentName, + $mgAscSecureScoreResult, + $subscriptionQuotaId, + $subscriptionState, + $subscriptionASCSecureScore, + $subscriptionTags, + $subscriptionTagsCount + ) + + $addRowToTableDone = $false + #Usage + $currentTask = "Getting Role assignments usage metrics for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleAssignmentsUsageMetrics?api-version=2019-08-01-preview" + $method = 'GET' + $roleAssignmentsUsage = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -listenOn 'Content' -caller 'CustomDataCollection' + + $script:htSubscriptionsRoleAssignmentLimit.($scopeId) = $roleAssignmentsUsage.roleAssignmentsLimit + + #PIM SubscriptionRoleAssignmentScheduleInstances + if ($htDoARMRoleAssignmentScheduleInstances.Do -eq $true) { + $currentTask = "Getting ARM RoleAssignment ScheduleInstances for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleAssignmentScheduleInstances?api-version=2020-10-01" + $method = 'GET' + $roleAssignmentScheduleInstancesFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + if ($roleAssignmentScheduleInstancesFromAPI -eq 'RoleAssignmentScheduleInstancesError' -or $roleAssignmentScheduleInstancesFromAPI -eq 'AadPremiumLicenseRequired') { + # this should not be required at sub level as the error would already have occured at mg level + # if ($roleAssignmentScheduleInstancesFromAPI -eq 'AadPremiumLicenseRequired') { + # Write-Host " Setting 'htDoARMRoleAssignmentScheduleInstances.Do' to false (AadPremiumLicenseRequired)" + # $script:htDoARMRoleAssignmentScheduleInstances.Do = $false + # } + } + else { + $roleAssignmentScheduleInstances = ($roleAssignmentScheduleInstancesFromAPI.where( { ($_.properties.roleAssignmentScheduleId -replace '.*/') -ne ($_.properties.originRoleAssignmentId -replace '.*/') })) + $roleAssignmentScheduleInstancesCount = $roleAssignmentScheduleInstances.Count + if ($roleAssignmentScheduleInstancesCount -gt 0) { + #$htRoleAssignmentsPIM = @{} + foreach ($roleAssignmentScheduleInstance in $roleAssignmentScheduleInstances) { + $script:htRoleAssignmentsPIM.($roleAssignmentScheduleInstance.properties.originRoleAssignmentId.tolower()) = $roleAssignmentScheduleInstance.properties + } + } + } + } + + #RoleAssignment API Sub + $currentTask = "Getting Role assignments API for Subscription: '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + $uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/subscriptions/$($scopeId)/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" + $method = 'GET' + $roleAssignmentsFromAPI = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection' + + $baseRoleAssignments = [System.Collections.ArrayList]@() + if ($roleAssignmentsFromAPI.Count -gt 0) { + foreach ($roleAssignmentFromAPI in $roleAssignmentsFromAPI) { + + if ($roleAssignmentFromAPI.id -match "/subscriptions/$($scopeId)/") { + if (-not $htRoleAssignmentsFromAPIInheritancePrevention.($roleAssignmentFromAPI.id -replace '.*/')) { + $null = $baseRoleAssignments.Add($roleAssignmentFromAPI) + } + else { + $null = $baseRoleAssignments.Add($htRoleAssignmentsFromAPIInheritancePrevention.($roleAssignmentFromAPI.id -replace '.*/').assignment) + } + } + else { + $null = $baseRoleAssignments.Add($roleAssignmentFromAPI) + } + } + } + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC -eq $true) { + $relevantRAs = $baseRoleAssignments.where( { $_.id -notmatch "/subscriptions/$($scopeId)/resourcegroups/" } ) + } + else { + $relevantRAs = $baseRoleAssignments + } + if ($relevantRAs.Count -gt 0) { + $principalsToResolve = @() + $principalsToResolve = foreach ($ra in $relevantRAs.properties | Sort-Object -Property principalId -Unique) { + if (-not $htPrincipals.($ra.principalId)) { + $ra.principalId + } + } + + if ($principalsToResolve.Count -gt 0) { + ResolveObjectIds -objectIds $principalsToResolve + } + } + + + $L1mgmtGroupSubRoleAssignments = $baseRoleAssignments + + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC -eq $true) { + foreach ($L1mgmtGroupSubRoleAssignmentOnRg in $L1mgmtGroupSubRoleAssignments.where( { $_.id -match "/subscriptions/$($scopeId)/resourcegroups/" } )) { + if (-not ($htCacheAssignmentsRBACOnResourceGroupsAndResources).($L1mgmtGroupSubRoleAssignmentOnRg.id)) { + + $roleDefinitionId = $L1mgmtGroupSubRoleAssignmentOnRg.properties.roleDefinitionId + $roleDefinitionIdGuid = $roleDefinitionId -replace '.*/' + + if (-not ($htCacheDefinitionsRole).($roleDefinitionIdGuid)) { + $roleAssignmentsRoleDefinition = '' + $roleDefinitionName = "'This roleDefinition likely was deleted although a roleAssignment existed'" + } + else { + $roleAssignmentsRoleDefinition = ($htCacheDefinitionsRole).($roleDefinitionIdGuid) + $roleDefinitionName = $roleAssignmentsRoleDefinition.Name + } + + #assignment + $arrayRoleAssignment = [System.Collections.ArrayList]@() + $null = $arrayRoleAssignment.Add([PSCustomObject]@{ + RoleAssignmentId = $L1mgmtGroupSubRoleAssignmentOnRg.id + Scope = $L1mgmtGroupSubRoleAssignmentOnRg.properties.scope + RoleDefinitionName = $roleDefinitionName + RoleDefinitionId = $L1mgmtGroupSubRoleAssignmentOnRg.properties.roleDefinitionId -replace '.*/' + ObjectId = $L1mgmtGroupSubRoleAssignmentOnRg.properties.principalId + }) + + ($script:htCacheAssignmentsRBACOnResourceGroupsAndResources).($L1mgmtGroupSubRoleAssignmentOnRg.id) = $arrayRoleAssignment + } + } + } + + if ($azAPICallConf['htParameters'].LargeTenant -eq $true -or $azAPICallConf['htParameters'].RBACAtScopeOnly -eq $true) { + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC -eq $false) { + $assignmentsScope = $L1mgmtGroupSubRoleAssignments + } + else { + $assignmentsScope = $L1mgmtGroupSubRoleAssignments.where( { $_.properties.Scope -eq "/subscriptions/$($scopeId)" } ) + } + } + else { + if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC -eq $false) { + $assignmentsScope = $L1mgmtGroupSubRoleAssignments + } + else { + $assignmentsScope = $L1mgmtGroupSubRoleAssignments.where( { $_.id -notmatch "/subscriptions/$($scopeId)/resourcegroups/" } ) + } + } + + foreach ($L1mgmtGroupSubRoleAssignment in $assignmentsScope) { + + $roleAssignmentId = ($L1mgmtGroupSubRoleAssignment.id).ToLower() + $roleDefinitionId = $L1mgmtGroupSubRoleAssignment.properties.roleDefinitionId + $roleDefinitionIdGuid = $roleDefinitionId -replace '.*/' + + if (-not ($htCacheDefinitionsRole).($roleDefinitionIdGuid)) { + $roleAssignmentsRoleDefinition = '' + $roleDefinitionName = "'This roleDefinition likely was deleted although a roleAssignment existed'" + } + else { + $roleAssignmentsRoleDefinition = ($htCacheDefinitionsRole).($roleDefinitionIdGuid) + $roleDefinitionName = $roleAssignmentsRoleDefinition.Name + } + + $roleAssignmentIdentityObjectId = $L1mgmtGroupSubRoleAssignment.properties.principalId + $roleAssignmentIdentityObjectType = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).type + $roleAssignmentScope = $L1mgmtGroupSubRoleAssignment.properties.scope + $roleAssignmentScopeName = $roleAssignmentScope -replace '.*/' + + if ($roleAssignmentScope -like '/subscriptions/*' -and $roleAssignmentScope -notlike '/subscriptions/*/resourcegroups/*') { + $roleAssignmentScopeType = 'Sub' + $roleAssignmentScopeRG = '' + $roleAssignmentScopeRes = '' + } + if ($roleAssignmentScope -like '/subscriptions/*/resourcegroups/*' -and $roleAssignmentScope -notlike '/subscriptions/*/resourcegroups/*/providers*') { + $roleAssignmentScopeType = 'RG' + $roleAssignmentScopeSplit = $roleAssignmentScope.Split('/') + $roleAssignmentScopeRG = $roleAssignmentScopeSplit[4] + $roleAssignmentScopeRes = '' + } + if ($roleAssignmentScope -like '/subscriptions/*/resourcegroups/*/providers*') { + $roleAssignmentScopeType = 'Res' + $roleAssignmentScopeSplit = $roleAssignmentScope.Split('/') + $roleAssignmentScopeRG = $roleAssignmentScopeSplit[4] + $roleAssignmentScopeRes = $roleAssignmentScopeSplit[8] + } + + if ($htRoleAssignmentsPIM.($roleAssignmentId)) { + $hlperPim = $htRoleAssignmentsPIM.($roleAssignmentId) + $pim = 'true' + $pimAssignmentType = $hlperPim.assignmentType + $pimSlotStart = $($hlperPim.startDateTime) + if ($hlperPim.endDateTime) { + $pimSlotEnd = $($hlperPim.endDateTime) + } + else { + $pimSlotEnd = 'eternity' + } + } + else { + $pim = 'false' + $pimAssignmentType = '' + $pimSlotStart = '' + $pimSlotEnd = '' + } + + if ($roleAssignmentId -like "/subscriptions/$($scopeId)/*") { + + #assignment + $splitAssignment = ($roleAssignmentId).Split('/') + $arrayRoleAssignment = [System.Collections.ArrayList]@() + $null = $arrayRoleAssignment.Add([PSCustomObject]@{ + RoleAssignmentId = $roleAssignmentId + Scope = $L1mgmtGroupSubRoleAssignment.properties.scope + DisplayName = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).displayName + SignInName = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).signInName + RoleDefinitionName = $roleDefinitionName + RoleDefinitionId = $L1mgmtGroupSubRoleAssignment.properties.roleDefinitionId -replace '.*/' + ObjectId = $L1mgmtGroupSubRoleAssignment.properties.principalId + ObjectType = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).type + PIM = $pim + }) + + $htTemp = @{} + $htTemp.Assignment = $arrayRoleAssignment + + $htTemp.AssignmentScopeTenMgSubRgRes = $roleAssignmentScopeType + if ($roleAssignmentScopeType -eq 'Sub') { + $htTemp.AssignmentScopeId = [string]$splitAssignment[2] + } + if ($roleAssignmentScopeType -eq 'RG') { + $htTemp.AssignmentScopeId = "$($splitAssignment[2])/$($splitAssignment[4])" + } + if ($roleAssignmentScopeType -eq 'Res') { + $htTemp.AssignmentScopeId = "$($splitAssignment[2])/$($splitAssignment[4])/$($splitAssignment[8])" + $htTemp.ResourceType = "$($splitAssignment[6])-$($splitAssignment[7])" + } + ($script:htCacheAssignmentsRole).($roleAssignmentId) = $htTemp + } + + + if (($htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).displayName).length -eq 0) { + $roleAssignmentIdentityDisplayname = 'n/a' + } + else { + if ($htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentityDisplayname = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).displayName + } + else { + $roleAssignmentIdentityDisplayname = 'scrubbed' + } + } + else { + $roleAssignmentIdentityDisplayname = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).displayName + } + } + if (-not $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).signInName) { + $roleAssignmentIdentitySignInName = 'n/a' + } + else { + if ($htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).type -eq 'User') { + if ($azAPICallConf['htParameters'].DoNotShowRoleAssignmentsUserData -eq $false) { + $roleAssignmentIdentitySignInName = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).signInName + } + else { + $roleAssignmentIdentitySignInName = 'scrubbed' + } + } + else { + $roleAssignmentIdentitySignInName = $htPrincipals.($L1mgmtGroupSubRoleAssignment.properties.principalId).signInName + } + } + + $roleSecurityCustomRoleOwner = 0 + if (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True) { + $roleSecurityCustomRoleOwner = 1 + } + $roleSecurityOwnerAssignmentSP = 0 + if ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Id -eq '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal') -or (($htCacheDefinitionsRole).$($roleDefinitionIdGuid).Actions -eq '*' -and ((($htCacheDefinitionsRole).$($roleDefinitionIdGuid).NotActions)).length -eq 0 -and ($htCacheDefinitionsRole).$($roleDefinitionIdGuid).IsCustom -eq $True -and $roleAssignmentIdentityObjectType -eq 'ServicePrincipal')) { + $roleSecurityOwnerAssignmentSP = 1 + } + + $createdBy = '' + $createdOn = '' + $createdOnUnformatted = $null + $updatedBy = '' + $updatedOn = '' + + if ($L1mgmtGroupSubRoleAssignment.properties.createdBy) { + $createdBy = $L1mgmtGroupSubRoleAssignment.properties.createdBy + } + if ($L1mgmtGroupSubRoleAssignment.properties.createdOn) { + $createdOn = $L1mgmtGroupSubRoleAssignment.properties.createdOn + } + if ($L1mgmtGroupSubRoleAssignment.properties.updatedBy) { + $updatedBy = $L1mgmtGroupSubRoleAssignment.properties.updatedBy + } + if ($L1mgmtGroupSubRoleAssignment.properties.updatedOn) { + $updatedOn = $L1mgmtGroupSubRoleAssignment.properties.updatedOn + } + $createdOnUnformatted = $L1mgmtGroupSubRoleAssignment.properties.createdOn + + $addRowToTableDone = $true + addRowToTable ` + -level $hierarchyLevel ` + -mgName $childMgDisplayName ` + -mgId $childMgId ` + -mgParentId $childMgParentId ` + -mgParentName $childMgParentName ` + -mgASCSecureScore $mgAscSecureScoreResult ` + -Subscription $scopeDisplayName ` + -SubscriptionId $scopeId ` + -SubscriptionQuotaId $subscriptionQuotaId ` + -SubscriptionState $subscriptionState ` + -SubscriptionASCSecureScore $subscriptionASCSecureScore ` + -SubscriptionTags $subscriptionTags ` + -SubscriptionTagsCount $subscriptionTagsCount ` + -RoleDefinitionId $roleDefinitionIdGuid ` + -RoleDefinitionName $roleDefinitionName ` + -RoleIsCustom $roleAssignmentsRoleDefinition.IsCustom ` + -RoleAssignableScopes ($roleAssignmentsRoleDefinition.AssignableScopes -join "$CsvDelimiterOpposite ") ` + -RoleActions ($roleAssignmentsRoleDefinition.Actions -join "$CsvDelimiterOpposite ") ` + -RoleNotActions ($roleAssignmentsRoleDefinition.NotActions -join "$CsvDelimiterOpposite ") ` + -RoleDataActions ($roleAssignmentsRoleDefinition.DataActions -join "$CsvDelimiterOpposite ") ` + -RoleNotDataActions ($roleAssignmentsRoleDefinition.NotDataActions -join "$CsvDelimiterOpposite ") ` + -RoleCanDoRoleAssignments $roleAssignmentsRoleDefinition.RoleCanDoRoleAssignments ` + -RoleAssignmentIdentityDisplayname $roleAssignmentIdentityDisplayname ` + -RoleAssignmentIdentitySignInName $roleAssignmentIdentitySignInName ` + -RoleAssignmentIdentityObjectId $roleAssignmentIdentityObjectId ` + -RoleAssignmentIdentityObjectType $roleAssignmentIdentityObjectType ` + -RoleAssignmentId $roleAssignmentId ` + -RoleAssignmentScope $roleAssignmentScope ` + -RoleAssignmentScopeName $roleAssignmentScopeName ` + -RoleAssignmentScopeRG $roleAssignmentScopeRG ` + -RoleAssignmentScopeRes $roleAssignmentScopeRes ` + -RoleAssignmentScopeType $roleAssignmentScopeType ` + -RoleAssignmentCreatedBy $createdBy ` + -RoleAssignmentCreatedOn $createdOn ` + -RoleAssignmentCreatedOnUnformatted $createdOnUnformatted ` + -RoleAssignmentUpdatedBy $updatedBy ` + -RoleAssignmentUpdatedOn $updatedOn ` + -RoleAssignmentsLimit $roleAssignmentsUsage.roleAssignmentsLimit ` + -RoleAssignmentsCount $roleAssignmentsUsage.roleAssignmentsCurrentCount ` + -RoleSecurityCustomRoleOwner $roleSecurityCustomRoleOwner ` + -RoleSecurityOwnerAssignmentSP $roleSecurityOwnerAssignmentSP ` + -RoleAssignmentPIM $pim ` + -RoleAssignmentPIMAssignmentType $pimAssignmentType ` + -RoleAssignmentPIMSlotStart $pimSlotStart ` + -RoleAssignmentPIMSlotEnd $pimSlotEnd + } + + $returnObject = @{} + if ($addRowToTableDone) { + $returnObject.'addRowToTableDone' = @{} + } + return $returnObject +} +$funcDataCollectionRoleAssignmentsSub = $function:dataCollectionRoleAssignmentsSub.ToString() + +function dataCollectionClassicAdministratorsSub { + [CmdletBinding()]Param( + [string]$scopeId, + [string]$scopeDisplayName, + [string]$subscriptionMgPath, + $subscriptionQuotaId + ) + + $apiEndPoint = $azAPICallConf['azAPIEndpointUrls'].ARM + $api = "/subscriptions/$($scopeId)/providers/Microsoft.Authorization/classicAdministrators" + $apiVersion = '?api-version=2015-07-01' + $uri = $apiEndPoint + $api + $apiVersion + $azAPICallPayload = @{ + uri = $uri + method = 'GET' + currentTask = "classicAdministrators '$($scopeDisplayName)' ('$scopeId') [quotaId:'$subscriptionQuotaId']" + AzAPICallConfiguration = $azAPICallConf + } + + $AzApiCallResult = AzAPICall @azAPICallPayload + if ($AzApiCallResult -ne 'ClassicAdministratorListFailed') { + $arrayClassicAdministrators = [System.Collections.ArrayList]@() + foreach ($roleAll in $AzApiCallResult) { + $splitPropertiesRole = $roleAll.properties.role.Split(';') + foreach ($role in $splitPropertiesRole) { + $null = $arrayClassicAdministrators.Add([PSCustomObject]@{ + Subscription = $scopeDisplayName + SubscriptionId = $scopeId + SubscriptionMgPath = $subscriptionMgPath + Identity = $roleAll.properties.emailAddress + Role = $role + Id = $roleAll.id + }) + } + } + $script:htClassicAdministrators.($scopeId) = @{} + $script:htClassicAdministrators.($scopeId).ClassicAdministrators = $arrayClassicAdministrators + } +} +$funcDataCollectionClassicAdministratorsSub = $function:dataCollectionClassicAdministratorsSub.ToString() + +#endregion functions4DataCollection +#region HTML +function HierarchyMgHTML($mgChild) { + $mgDetails = $htMgDetails.($mgChild).details + $mgName = $mgDetails.mgName + $mgId = $mgDetails.MgId + + if ($mgId -eq ($azAPICallConf['checkContext']).Tenant.Id) { + if ($mgId -eq $defaultManagementGroupId) { + $class = "class=`"tenantRootGroup mgnonradius defaultMG`"" + } + else { + $class = "class=`"tenantRootGroup mgnonradius`"" + } + } + else { + if ($mgId -eq $defaultManagementGroupId) { + $class = "class=`"mgnonradius defaultMG`"" + } + else { + $class = "class=`"mgnonradius`"" + } + $liclass = '' + $liId = '' + } + if ($mgName -eq $mgId) { + $mgNameAndOrId = $mgName -replace '<', '<' -replace '>', '>' + } + else { + $mgNameAndOrId = "$($mgName -replace '<', '<' -replace '>', '>')
$mgId" + } + + $mgPolicyAssignmentCount = 0 + if ($htMgAtScopePolicyAssignments.($mgId)) { + $mgPolicyAssignmentCount = $htMgAtScopePolicyAssignments.($mgId).AssignmentsCount + } + $mgPolicyPolicySetScopedCount = 0 + if ($htMgAtScopePoliciesScoped.($mgId)) { + $mgPolicyPolicySetScopedCount = $htMgAtScopePoliciesScoped.($mgId).ScopedCount + } + $mgIdRoleAssignmentCount = 0 + if ($htMgAtScopeRoleAssignments.($mgId)) { + $mgIdRoleAssignmentCount = $htMgAtScopeRoleAssignments.($mgId).AssignmentsCount + } + $script:html += @" +
  • + +
    + +
    +
    +"@ + if ($mgPolicyAssignmentCount -gt 0 -or $mgPolicyPolicySetScopedCount -gt 0) { + if ($mgPolicyAssignmentCount -gt 0 -and $mgPolicyPolicySetScopedCount -gt 0) { + $script:html += @" +
    + $($mgPolicyAssignmentCount) +
    +
    + $($mgPolicyPolicySetScopedCount) +
    +"@ + } + else { + if ($mgPolicyAssignmentCount -gt 0) { + $script:html += @" +
    + $($mgPolicyAssignmentCount) +
    +"@ + } + if ($mgPolicyPolicySetScopedCount -gt 0) { + $script:html += @" +
    + $($mgPolicyPolicySetScopedCount) +
    +"@ + } + } + } + else { + $script:html += @' +
    +'@ + } + $script:html += @' +
    + +
    +'@ + if ($mgIdRoleAssignmentCount -gt 0) { + $script:html += @" +
    + $($mgIdRoleAssignmentCount) +
    +"@ + } + else { + $script:html += @' +
    +'@ + } + $script:html += @" +
    +
    + +
    $($mgNameAndOrId) +
    +
    +
    +"@ + $childMgs = $htMgDetails.($mgId).mgChildren + if (($childMgs).count -gt 0) { + $script:html += @' +
      +'@ + foreach ($childMg in $childMgs) { + HierarchyMgHTML -mgChild $childMg + } + HierarchySubForMgHTML -mgChild $mgId + $script:html += @' +
    +
  • +'@ + } + else { + HierarchySubForMgUlHTML -mgChild $mgId + $script:html += @' + +'@ + } +} + +function HierarchySubForMgHTML($mgChild) { + $subscriptions = $htMgDetails.($mgChild).Subscriptions.SubScriptionId + $subscriptionsCnt = ($subscriptions).count + $subscriptionsOutOfScopelinked = $outOfScopeSubscriptions.where( { $_.ManagementGroupId -eq $mgChild } ) + $subscriptionsOutOfScopelinkedCnt = ($subscriptionsOutOfScopelinked).count + Write-Host " Building HierarchyMap for MG '$mgChild', $($subscriptionsCnt) Subscriptions" + if ($subscriptionsCnt -gt 0 -or $subscriptionsOutOfScopelinkedCnt -gt 0) { + if ($subscriptionsCnt -gt 0 -and $subscriptionsOutOfScopelinkedCnt -gt 0) { + $script:html += @" +
  • $(($subscriptions).count)x $(($subscriptionsOutOfScopelinked).count)x
  • +"@ + } + if ($subscriptionsCnt -gt 0 -and $subscriptionsOutOfScopelinkedCnt -eq 0) { + $script:html += @" +
  • $(($subscriptions).count)x
  • +"@ + } + if ($subscriptionsCnt -eq 0 -and $subscriptionsOutOfScopelinkedCnt -gt 0) { + $script:html += @" +
  • $(($subscriptionsOutOfScopelinked).count)x
  • +"@ + } + } +} + +function HierarchySubForMgUlHTML($mgChild) { + $subscriptions = $htMgDetails.($mgChild).Subscriptions.SubScriptionId + $subscriptionsCnt = ($subscriptions).count + $subscriptionsOutOfScopelinked = $outOfScopeSubscriptions.where( { $_.ManagementGroupId -eq $mgChild } ) + $subscriptionsOutOfScopelinkedCnt = ($subscriptionsOutOfScopelinked).count + Write-Host " Building HierarchyMap for MG '$mgChild', $($subscriptionsCnt) Subscriptions" + if ($subscriptionsCnt -gt 0 -or $subscriptionsOutOfScopelinkedCnt -gt 0) { + if ($subscriptionsCnt -gt 0 -and $subscriptionsOutOfScopelinkedCnt -gt 0) { + $script:html += @" + +"@ + } + if ($subscriptionsCnt -gt 0 -and $subscriptionsOutOfScopelinkedCnt -eq 0) { + $script:html += @" + +"@ + } + if ($subscriptionsCnt -eq 0 -and $subscriptionsOutOfScopelinkedCnt -gt 0) { + $script:html += @" + +"@ + } + } +} + +function processScopeInsights($mgChild, $mgChildOf) { + $mgDetails = $htMgDetails.($mgChild).details + $mgName = $mgDetails.mgName + $mgLevel = $mgDetails.Level + $mgId = $mgDetails.MgId + + if (-not $NoScopeInsights) { + if ($mgId -eq $defaultManagementGroupId) { + $classDefaultMG = 'defaultMG' + } + else { + $classDefaultMG = '' + } + + switch ($mgLevel) { + '0' { $levelSpacing = '| L0 – ' } + '1' { $levelSpacing = '|     – L1 – ' } + '2' { $levelSpacing = '|         – – L2 – ' } + '3' { $levelSpacing = '|             – – – L3 – ' } + '4' { $levelSpacing = '|                 – – – – L4 – ' } + '5' { $levelSpacing = '|                     – – – – – L5 – ' } + '6' { $levelSpacing = '|                         – – – – – – L6 – ' } + } + + $mgPath = $htManagementGroupsMgPath.($mgChild).pathDelimited + + $mgLinkedSubsCount = ((($optimizedTableForPathQuery.where( { $_.MgId -eq $mgChild -and -not [String]::IsNullOrEmpty($_.SubscriptionId) } )).SubscriptionId | Get-Unique)).count + $subscriptionsOutOfScopelinkedCount = ($outOfScopeSubscriptions.where( { $_.ManagementGroupId -eq $mgChild } )).count + if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { + $subInfo = "$mgLinkedSubsCount" + } + if ($mgLinkedSubsCount -gt 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { + $subInfo = "$mgLinkedSubsCount $subscriptionsOutOfScopelinkedCount" + } + if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -gt 0) { + $subInfo = "$subscriptionsOutOfScopelinkedCount" + } + if ($mgLinkedSubsCount -eq 0 -and $subscriptionsOutOfScopelinkedCount -eq 0) { + $subInfo = "" + } + + if ($mgName -eq $mgId) { + $mgNameAndOrId = "$($mgName -replace '<', '<' -replace '>', '>')" + } + else { + $mgNameAndOrId = "$($mgName -replace '<', '<' -replace '>', '>') ($mgId)" + } + + $script:html += @" + +
    + + +"@ + if ($mgId -eq $defaultManagementGroupId) { + $script:html += @' + +'@ + } + $script:html += @" + + + +"@ + } + processScopeInsightsMgOrSub -mgOrSub 'mg' -mgchild $mgId + processScopeInsightsMGSubs -mgChild $mgId + $childMgs = $htMgDetails.($mgId).mgChildren + if (($childMgs).count -gt 0) { + foreach ($childMg in $childMgs) { + processScopeInsights -mgChild $childMg -mgChildOf $mgId + } + } +} + +function processScopeInsightsMGSubs($mgChild) { + $subscriptions = $htMgDetails.($mgChild).Subscriptions + $subscriptionLinkedCount = ($subscriptions).count + $subscriptionsOutOfScopelinked = $outOfScopeSubscriptions.where( { $_.ManagementGroupId -eq $mgChild } ) + $subscriptionsOutOfScopelinkedCount = ($subscriptionsOutOfScopelinked).count + if ($subscriptionsOutOfScopelinkedCount -gt 0) { + $subscriptionsOutOfScopelinkedDetail = "($($subscriptionsOutOfScopelinkedCount) out-of-scope)" + } + else { + $subscriptionsOutOfScopelinkedDetail = '' + } + Write-Host " Building ScopeInsights MG '$mgChild', $subscriptionLinkedCount Subscriptions" + + if ($subscriptionLinkedCount -gt 0) { + if (-not $NoScopeInsights) { + $script:html += @" + + + + + +
    Highlight Management Group in HierarchyMap

    Default Management Group docs

    Management Group Name: $($mgName -replace '<', '<' -replace '>', '>')

    Management Group Id: $mgId

    Management Group Path: $mgPath

    + +
    +"@ + } + foreach ($subEntry in $subscriptions | Sort-Object -Property subscription, subscriptionId) { + #$subPath = $htSubscriptionsMgPath.($subEntry.subscriptionId).pathDelimited + if ($subscriptionLinkedCount -gt 1) { + if (-not $NoScopeInsights) { + $script:html += @" + +
    +"@ + } + } + #exactly 1 + else { + if (-not $NoScopeInsights) { + $script:html += @" + $($subEntry.subscription -replace '<', '<' -replace '>', '>') ($($subEntry.subscriptionId)) +"@ + } + } + if (-not $NoScopeInsights) { + $script:html += @" + + +"@ + } + + if (-not $azAPICallConf['htParameters'].ManagementGroupsOnly) { + processScopeInsightsMgOrSub -mgOrSub 'sub' -subscriptionId $subEntry.subscriptionId -subscriptionsMgId $mgChild + } + + if (-not $NoScopeInsights) { + $script:html += @' +
    Highlight Subscription in HierarchyMap
    +'@ + } + if ($subscriptionLinkedCount -gt 1) { + if (-not $NoScopeInsights) { + $script:html += @' +
    +'@ + } + } + } + if (-not $NoScopeInsights) { + $script:html += @' +
    +'@ + } + + } + else { + if (-not $NoScopeInsights) { + $script:html += @" +
    + $subscriptionLinkedCount Subscriptions linked $subscriptionsOutOfScopelinkedDetail +"@ + } + } + if (-not $NoScopeInsights) { + $script:html += @' +
    +
    +'@ + } +} +#endregion HTML #endregion Functions $funcAddRowToTable = $function:addRowToTable.ToString() From 05995199a9eab308c1340fa4cd38e56f2ff7ead5 Mon Sep 17 00:00:00 2001 From: Kai Schulz Date: Fri, 15 Dec 2023 14:42:31 +0100 Subject: [PATCH 3/5] final test --- pwsh/AzGovVizParallel.ps1 | 22 ++-------------------- version.json | 2 +- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 154805ac..e042db2d 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -12461,16 +12461,7 @@ function processStorageAccountAnalysis { } else { try { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if($saProperties.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$saProperties - $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) - } - - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions - + $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { $staticWebsitesState = $true @@ -12503,16 +12494,7 @@ function processStorageAccountAnalysis { } if ($listContainersSuccess -eq $true) { - # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 - if($listContainers.gettype().Name -eq 'Byte[]') { - $byteArray = [byte[]]$listContainers - $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) - } - - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) - # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) - $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions - + $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { diff --git a/version.json b/version.json index 3f21333e..ca651092 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "ProductVersion": "6.3.4" + "ProductVersion": "6.3.5" } \ No newline at end of file From 8813de63d0d59183391cb9a1490c09be0cdd7217 Mon Sep 17 00:00:00 2001 From: Kai Schulz Date: Fri, 15 Dec 2023 16:20:48 +0100 Subject: [PATCH 4/5] Function edit --- pwsh/AzGovVizParallel.ps1 | 20 +++++++++++++++++-- .../processStorageAccountAnalysis.ps1 | 20 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index e042db2d..97df6c26 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -12461,7 +12461,15 @@ function processStorageAccountAnalysis { } else { try { - $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { $staticWebsitesState = $true @@ -12494,7 +12502,15 @@ function processStorageAccountAnalysis { } if ($listContainersSuccess -eq $true) { - $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { diff --git a/pwsh/dev/functions/processStorageAccountAnalysis.ps1 b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 index 7177bfbf..3ed16835 100644 --- a/pwsh/dev/functions/processStorageAccountAnalysis.ps1 +++ b/pwsh/dev/functions/processStorageAccountAnalysis.ps1 @@ -59,7 +59,15 @@ function processStorageAccountAnalysis { } else { try { - $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($saProperties.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$saProperties + $saProperties = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlSaProperties = [xml]([string]$saProperties -replace $saProperties.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlSaProperties = [xml]($saProperties -replace '^.*?<', '<') # Universal fix for all PS versions if ($xmlSaProperties.StorageServiceProperties.StaticWebsite) { if ($xmlSaProperties.StorageServiceProperties.StaticWebsite.Enabled -eq $true) { $staticWebsitesState = $true @@ -92,7 +100,15 @@ function processStorageAccountAnalysis { } if ($listContainersSuccess -eq $true) { - $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) + # ? https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/issues/218#issuecomment-1854516882 + if ($listContainers.gettype().Name -eq 'Byte[]') { + $byteArray = [byte[]]$listContainers + $listContainers = [System.Text.Encoding]::UTF8.GetString($byteArray) + } + + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 3)) # Leading character:  (PS version <= 7.3.9) + # $xmlListContainers = [xml]([string]$listContainers -replace $listContainers.Substring(0, 1)) # Leading character:  or U+feff (PS version >= 7.4.0) + $xmlListContainers = [xml]($listContainers -replace '^.*?<', '<') # Universal fix for all PS versions $containersCount = $xmlListContainers.EnumerationResults.Containers.Container.Count foreach ($container in $xmlListContainers.EnumerationResults.Containers.Container) { From 9b1588613e05b2dbebb43cfd4868b222d3581156 Mon Sep 17 00:00:00 2001 From: Kai Schulz Date: Fri, 15 Dec 2023 16:23:18 +0100 Subject: [PATCH 5/5] AzAPICallVersion = '1.1.85' --- pwsh/AzGovVizParallel.ps1 | 2 +- pwsh/dev/devAzGovVizParallel.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 97df6c26..6b01c6bc 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -372,7 +372,7 @@ Param # <--- AzAPICall related parameters #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall [string] - $AzAPICallVersion = '1.1.84', + $AzAPICallVersion = '1.1.85', [switch] $DebugAzAPICall, diff --git a/pwsh/dev/devAzGovVizParallel.ps1 b/pwsh/dev/devAzGovVizParallel.ps1 index 2387c3b7..cfbc48c4 100644 --- a/pwsh/dev/devAzGovVizParallel.ps1 +++ b/pwsh/dev/devAzGovVizParallel.ps1 @@ -372,7 +372,7 @@ Param # <--- AzAPICall related parameters #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall [string] - $AzAPICallVersion = '1.1.84', + $AzAPICallVersion = '1.1.85', [switch] $DebugAzAPICall,