pwsh/dev/devAzGovVizParallel.ps1 (2,193 lines of code) (raw):
<#
.SYNOPSIS
This script creates the following files to help better understand and audit your governance setup
csv file
Management Groups, Subscriptions, Policy, PolicySet (Initiative), RBAC
html file
Management Groups, Subscriptions, Policy, PolicySet (Initiative), RBAC
The html file uses Java Script and CSS files which are hosted on various CDNs (Content Delivery Network). For details review the BuildHTML region in this script.
markdown file for use with Azure DevOps Wiki leveraging the Mermaid plugin
Management Groups, Subscriptions
.DESCRIPTION
Do you want to get granular insights on your technical Azure Governance implementation? - document it in csv, html and markdown? Azure Governance Visualizer is a PowerShell based script that iterates your Azure Tenants Management Group hierarchy down to Subscription level. It captures most relevant Azure governance capabilities such as Azure Policy, RBAC and Blueprints and a lot more. From the collected data Azure Governance Visualizer provides visibility on your Hierarchy Map, creates a Tenant Summary and builds granular Scope Insights on Management Groups and Subscriptions. The technical requirements as well as the required permissions are minimal.
.PARAMETER ManagementGroupId
Define the Management Group Id for which the outputs/files should be generated
.PARAMETER CsvDelimiter
The script outputs a csv file depending on your delimit defaults choose semicolon or comma
.PARAMETER OutputPath
Full- or relative path
.PARAMETER DoNotShowRoleAssignmentsUserData
default is to capture the DisplayName and SignInName for RoleAssignments on ObjectType=User; for data protection and security reasons this may not be acceptable
.PARAMETER HierarchyMapOnly
default is to query all Management groups and Subscription for Governance capabilities, if you use the parameter -HierarchyMapOnly then only the HierarchyMap will be created
.PARAMETER NoMDfCSecureScore
default is to query all Subscriptions for Azure Microsoft Defender for Cloud Secure Score and summarize Secure Score for Management Groups.
.PARAMETER LimitCriticalPercentage
default is 80%, this parameter defines the warning level for approaching Limits (e.g. 80% of Role Assignment limit reached) change as per your preference
.PARAMETER SubscriptionQuotaIdWhitelist
default is 'undefined', this parameter defines the QuotaIds the subscriptions must match so that Azure Governance Visualizer processes them. The script checks if the QuotaId startswith the string that you have put in. Separate multiple strings with comma e.g. MSDN_,EnterpriseAgreement_
.PARAMETER SubscriptionIdWhitelist
default is 'undefined', this parameter defines the subscriptions that must match in order for the Azure Governance Visualizer to process them. Separate multiple strings with comma e.g. 2f4a9838-26b7-47ee-be60-ccc1fdec5953,33e01921-4d64-4f8c-a055-5bdaffd5e33d
.PARAMETER NoPolicyComplianceStates
use this parameter if policy compliance states should not be queried
.PARAMETER NoResourceDiagnosticsPolicyLifecycle
use this parameter if Resource Diagnostics Policy Lifecycle recommendations should not be created
.PARAMETER NoAADGroupsResolveMembers
use this parameter if Microsoft Entra ID Group memberships should not be resolved for Role assignments where identity type is 'Group'
.PARAMETER AADServicePrincipalExpiryWarningDays
define Service Principal Secret and Certificate grace period (lifetime below the defined will be marked for warning / default is 14 days)
.PARAMETER NoAzureConsumption
#obsolete
use this parameter if Azure Consumption data should not be reported
.PARAMETER DoAzureConsumption
use this parameter if Azure Consumption data should be reported
.PARAMETER AzureConsumptionPeriod
use this parameter to define for which time period Azure Consumption data should be gathered; default is 1 day
.PARAMETER NoAzureConsumptionReportExportToCSV
use this parameter if Azure Consumption data should not be exported (CSV)
.PARAMETER ThrottleLimit
Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5)
.PARAMETER DoTranscript
Log the console output
.PARAMETER MermaidDirection
Define the direction the Mermaid based HierarchyMap should be built TD (default) = TopDown (Horizontal), LR = LeftRight (Vertical)
.PARAMETER SubscriptionId4AzContext
Define the Subscription Id to use for AzContext (default is to use a random Subscription Id) #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall
.PARAMETER TenantId4AzContext
Define the Tenant Id to use for AzContext. Default is to use the Tenant Id from the current context #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall
.PARAMETER NoCsvExport
Export enriched 'Role assignments' data, enriched 'Policy assignments' data and 'all resources' (subscriptionId, mgPath, resourceType, id, name, location, tags, createdTime, changedTime)
.PARAMETER DoNotIncludeResourceGroupsOnPolicy
Do not include Policy assignments on ResourceGroups
.PARAMETER DoNotIncludeResourceGroupsAndResourcesOnRBAC
Do not include Role assignments on ResourceGroups and Resources
.PARAMETER ChangeTrackingDays
Define the period for Change tracking on newly created and updated custom Policy, PolicySet and RBAC Role definitions and Policy/RBAC Role assignments (default is '14')
.PARAMETER FileTimeStampFormat
Ddefine the time format for the output files (default is `yyyyMMdd_HHmmss`)
.PARAMETER NoJsonExport
Enable export of ManagementGroup Hierarchy including all MG/Sub Policy/RBAC definitions, Policy/RBAC assignments and some more relevant information to JSON
.PARAMETER JsonExportExcludeResourceGroups
JSON Export will not include ResourceGroups (Policy & Role assignments)
.PARAMETER JsonExportExcludeResources
JSON Export will not include Resources (Role assignments)
.PARAMETER LargeTenant
A large tenant is a tenant with more than ~500 Subscriptions - the HTML output for large tenants simply becomes too big.
If the parameter switch is true then the following parameters will be set:
-PolicyAtScopeOnly $true
-RBACAtScopeOnly $true
-NoResourceProvidersAtAll $true
-NoScopeInsights $true
.PARAMETER PolicyAtScopeOnly
Removing 'inherited' lines in the HTML file; use this parameter if you run against a larger tenants
.PARAMETER RBACAtScopeOnly
Removing 'inherited' lines in the HTML file; use this parameter if you run against a larger tenants
.PARAMETER NoResourceProvidersDetailed
default is to output all ResourceProvider states for all Subscriptions in the TenantSummary. In large Tenants this can become time consuming and may blow off the html file.
.PARAMETER NoResourceProvidersAtAll
Note if you use parameter -LargeTenant then parameter NoResourceProvidersAtAll will be set to true
Resource Provider states will not be collected
.PARAMETER NoScopeInsights
Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true
Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size)
.PARAMETER AADGroupMembersLimit
Defines the limit (default=500) of Microsoft Entra group members; For Microsoft Entra groups that have more members than the defined limit group members will not be resolved
.PARAMETER NoResources
Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants)
.PARAMETER StatsOptOut
Will opt-out sending stats
.PARAMETER NoSingleSubscriptionOutput
Single Scope Insights output per Subscription should not be created
.PARAMETER HtmlTableRowsLimit
Although the parameter -LargeTenant was introduced recently, still the html output may become too large to be processed properly. The new parameter defines the limit of rows - if for the html processing part the limit is reached then the html table will not be created (csv and json output will still be created). Default rows limit is 20.000
.PARAMETER ManagementGroupsOnly
Collect data only for Management Groups (Subscription data such as e.g. Policy assignments etc. will not be collected)
.PARAMETER ExcludedResourceTypesDiagnosticsCapable
Resource Types to be excluded from processing analysis for diagnostic settings capability (default: microsoft.web/certificates)
.PARAMETER NoPIMEligibility
Do not report on PIM (Privileged Identity Management) eligible Role assignments
Note: this feature requires you to execute as Service Principal with `Application` API permission `PrivilegedAccess.Read.AzureResources`
.PARAMETER PIMEligibilityIgnoreScope
Ignore the current scope (ManagementGrouId) and get all PIM (Privileged Identity Management) eligible Role assignments
By default will only report for PIM Elibility for the scope (ManagementGroupId) that was provided. If you use the new switch parameter then PIM Eligibility for all onboarded scopes (Management Groups and Subscriptions) will be reported
.PARAMETER NoPIMEligibilityIntegrationRoleAssignmentsAll
Prevent integration of PIM eligible assignments with RoleAssignmentsAll (HTML, CSV)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoPIMEligibilityIntegrationRoleAssignmentsAll
.PARAMETER NoALZPolicyVersionChecker
'Azure Landing Zones (ALZ) Policy Version Checker' for Policy and Set definitions. Azure Governance Visualizer will clone the ALZ GitHub repository and collect the ALZ policy and set definitions history. The ALZ data will be compared with the data from your tenant so that you can get lifecycle management recommendations for ALZ policy and set definitions that already exist in your tenant plus a list of ALZ policy and set definitions that do not exist in your tenant. The 'Azure Landing Zones (ALZ) Policy Version Checker' results will be displayed in the TenantSummary and a CSV export `*_ALZPolicyVersionChecker.csv` will be provided.
If you do not want to execute the 'Azure Landing Zones (ALZ) Policy Version Checker' feature then use this parameter
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoALZPolicyVersionChecker
.PARAMETER NoDefinitionInsightsDedicatedHTML
DefinitionInsights will be written to a separate HTML file `*_DefinitionInsights.html`. If you want to keep DefinitionInsights in the main html file then use this parameter
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoDefinitionInsightsDedicatedHTML
.PARAMETER NoStorageAccountAccessAnalysis
Analysis on Storage Accounts, specially focused on anonymous access.
If you do not want to execute this feature then use this parameter
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoStorageAccountAccessAnalysis
.PARAMETER StorageAccountAccessAnalysisSubscriptionTags
If the Storage Account Access Analysis feature is executed with this parameter you can define the subscription tags that should be added to the CSV output
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail')
.PARAMETER StorageAccountAccessAnalysisStorageAccountTags
If the Storage Account Access Analysis feature is executed with this parameter you can define the Storage Account (resource) tags that should be added to the CSV output
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer')
.PARAMETER NoNetwork
Network analysis / Virtual Network, Subnets, Virtual Network Peerings and Private Endpoints
If you do not want to execute this feature then use this parameter
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoNetwork
.PARAMETER NetworkSubnetIPAddressUsageCriticalPercentage
Define warning level when ceratin percentage of IP addresses is used (default = 90%)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NetworkSubnetIPAddressUsageCriticalPercentage 96
.EXAMPLE
Define the ManagementGroup ID
PS C:\> .\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id>
Define how the CSV output should be delimited. Valid input is ; or , (semicolon is default)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -CsvDelimiter ","
Define the outputPath (must exist)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -OutputPath 123
Define if User information should be scrubbed (default prints Userinformation to the CSV and HTML output)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -DoNotShowRoleAssignmentsUserData
Define if only the HierarchyMap output should be created. Will ignore the parameters 'LimitCriticalPercentage' and 'DoNotShowRoleAssignmentsUserData' (default queries for Governance capabilities such as policy-, role-, blueprints assignments and more)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -HierarchyMapOnly
Define if Microsoft Defender for Cloud SecureScore should be queried for Subscriptions and Management Groups
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoMDfCSecureScore
Define when limits should be highlighted as warning (default is 80 percent)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -LimitCriticalPercentage 90
Define the QuotaId whitelist by providing strings separated by a comma
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -SubscriptionQuotaIdWhitelist MSDN_,EnterpriseAgreement_
Define the subscriptions whitelist by providing strings separated by a comma
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -SubscriptionIdWhitelist 2f4a9838-26b7-47ee-be60-ccc1fdec5953,33e01921-4d64-4f8c-a055-5bdaffd5e33d
Define if policy compliance states should be queried
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoPolicyComplianceStates
Define if Resource Diagnostics Policy Lifecycle recommendations should not be created
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoResourceDiagnosticsPolicyLifecycle
Define if Microsoft Entra ID Group memberships should not be resolved for Role assignments where identity type is 'Group'
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoAADGroupsResolveMembers
Define Service Principal Secret and Certificate grace period (lifetime below the defined will be marked for warning / default is 14 days)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -AADServicePrincipalExpiryWarningDays 30
#obsolete Define if Azure Consumption data should not be reported
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoAzureConsumption
Define if Azure Consumption data should be reported
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -DoAzureConsumption
Define for which time period (days) Azure Consumption data should be gathered; e.g. 14 days; default is 1 day
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -AzureConsumptionPeriod 14
Define the number of script blocks running in parallel. Leveraging PowerShell Core's parallel capability you can define the ThrottleLimit (default=5)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -ThrottleLimit 10
Define if you want to log the console output
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -DoTranscript
Define the direction the Mermaid based HierarchyMap should be built in Markdown TD = TopDown (Horizontal), LR = LeftRight (Vertical)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -MermaidDirection "LR"
Define the Subscription Id to use for AzContext (default is to use a random Subscription Id) #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -SubscriptionId4AzContext "<your-Subscription-Id>"
Define the Tenant Id to use for AzContext (default is to use the Tenant Id from the current context) #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -TenantId4AzContext "<your-Tenant-Id>"
Do not Export enriched 'Role assignments' data, enriched 'Policy assignments' data and 'all resources' (subscriptionId, mgPath, resourceType, id, name, location, tags, createdTime, changedTime)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoCsvExport
Do not include Policy assignments on ResourceGroups
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -DoNotIncludeResourceGroupsOnPolicy
Do not include Role assignments on ResourceGroups and Resources
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -DoNotIncludeResourceGroupsAndResourcesOnRBAC
Define the period for Change tracking on newly created and updated custom Policy, PolicySet and RBAC Role definitions and Policy/RBAC Role assignments (default is '14')
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -ChangeTrackingDays 30
Define the time format for the output files (default is `yyyyMMdd_HHmmss`)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -FileTimeStampFormat "yyyyMM-dd_HHmm" (default is `yyyyMMdd_HHmmss`)
Do not enable export of ManagementGroup Hierarchy including all MG/Sub Policy/RBAC definitions, Policy/RBAC assignments and some more relevant information to JSON
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoJsonExport
JSON Export will not include ResourceGroups (Policy & Role assignments)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -JsonExportExcludeResourceGroups
JSON Export will not include Resources (Role assignments)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -JsonExportExcludeResources
A large tenant is a tenant with more than ~500 Subscriptions - the HTML output for large tenants simply becomes too big.
If the parameter switch is true then the following parameters will be set:
-PolicyAtScopeOnly $true
-RBACAtScopeOnly $true
-NoResourceProvidersAtAll $true
-NoScopeInsights $true
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -LargeTenant
Removing 'inherited' lines in the HTML file for 'Policy Assignments'; use this parameter if you run against a larger tenants
Note if you use parameter -LargeTenant then parameter -PolicyAtScopeOnly will be set to true
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -PolicyAtScopeOnly
Removing 'inherited' lines in the HTML file for 'Role Assignments'; use this parameter if you run against a larger tenants
Note if you use parameter -LargeTenant then parameter -RBACAtScopeOnly will be set to true
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -RBACAtScopeOnly
Define if a detailed summary on Resource Provider states per Subscription should be created in the TenantSummary section
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoResourceProvidersDetailed
Define if Resource Provider states should be collected
Note if you use parameter -LargeTenant then parameter -NoResourceProvidersAtAll will be set to true
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoResourceProvidersAtAll
Define if ScopeInsights should be created or not. Q: Why would you want to do this? A: In larger tenants the ScopeInsights section blows up the html file (up to unusable due to html file size)
Note if you use parameter -LargeTenant then parameter -NoScopeInsights will be set to true
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoScopeInsights
Defines the limit (default=500) of Microsoft Entra group members; For groups that have more members than the defined limit group members will not be resolved
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -AADGroupMembersLimit 750
Will speed up the processing time but information like Resource diagnostics capability, resource type stats, UserAssigned Identities assigned to Resources is excluded (featured for large tenants)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoResources
Will opt-out sending stats
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -StatsOptOut
Will not create a single Scope Insights output per Subscription
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoSingleSubscriptionOutput
Although the parameter -LargeTenant was introduced recently, still the html output may become too large to be processed properly. The new parameter defines the limit of rows - if for the html processing part the limit is reached then the html table will not be created (csv and json output will still be created). Default rows limit is 20.000
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -HtmlTableRowsLimit 23077
Define if data should be collected for Management Groups only (Subscription data such as e.g. Policy assignments etc. will not be collected)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -ManagementGroupsOnly
Define Resource Types to be excluded from processing analysis for diagnostic settings capability (default: microsoft.web/certificates)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -ExcludedResourceTypesDiagnosticsCapable @('microsoft.web/certificates')
Define if report on PIM (Privileged Identity Management) eligible Role assignments should be created. Note: this feature requires you to execute as Service Principal with `Application` API permission `PrivilegedAccess.Read.AzureResources`
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoPIMEligibility
Define if the current scope (ManagementGroupId) should be ignored and therefore and get all PIM (Privileged Identity Management) eligible Role assignments. Note: this feature requires you to execute as Service Principal with `Application` API permission `PrivilegedAccess.Read.AzureResources`
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -PIMEligibilityIgnoreScope
Define if PIM Eligible assignments should not be integrated with RoleAssignmentsAll outputs (HTML, CSV)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoPIMEligibilityIntegrationRoleAssignmentsAll
Define if the 'Azure Landing Zones (ALZ) Policy Version Checker' feature should not be executed
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoALZPolicyVersionChecker
Define if DefinitionInsights should not be written to a seperate html file (*_DefinitionInsights.html)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoDefinitionInsightsDedicatedHTML
Define if Storage Account Access Analysis (focus on anonymous access) should be executed
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoStorageAccountAccessAnalysis
Additionally you can define Subscription and/or Storage Account Tag names that should be added to the CSV output per Storage Account
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> --StorageAccountAccessAnalysisSubscriptionTags @('Responsible', 'TeamEmail') -StorageAccountAccessAnalysisStorageAccountTags @('SAResponsible', 'DataOfficer')
Define if Network analysis / Virtual Network and Virtual Network Peerings should not be executed
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NoNetwork
Define warning level when ceratin percentage of IP addresses is used (default = 90%)
PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId <your-Management-Group-Id> -NetworkSubnetIPAddressUsageCriticalPercentage 96
.NOTES
AUTHOR: Julian Hayward - Customer Engineer - Customer Success Unit | Azure Infrastucture/Automation/Devops/Governance | Microsoft
.LINK
https://github.com/Azure/Azure-Governance-Visualizer (aka.ms/AzGovViz)
https://github.com/microsoft/CloudAdoptionFramework/tree/master/govern/AzureGovernanceVisualizer
Please note that while being developed by a Microsoft employee, Azure Governance Visualizer is not a Microsoft service or product. Azure Governance Visualizer is a personal/community driven project, there are none implicit or explicit obligations related to this project, it is provided 'as is' with no warranties and confer no rights.
#>
[CmdletBinding()]
Param
(
[string]
$Product = 'AzGovViz',
[string]
$ProductVersion = '6.6.1',
[string]
$GithubRepository = 'aka.ms/AzGovViz',
# <--- AzAPICall related parameters #consult the AzAPICall GitHub repository for details aka.ms/AzAPICall
[string]
$AzAPICallVersion = '1.2.4',
[switch]
$DebugAzAPICall,
[switch]
$AzAPICallSkipAzContextSubscriptionValidation,
[string]
$SubscriptionId4AzContext = 'undefined',
[string]
$TenantId4AzContext = 'undefined',
# AzAPICall related parameters --->
[string]
$ARMLocation = 'westeurope',
[string]
$ScriptPath = 'pwsh', #e.g. 'myfolder\pwsh'
[string]
$ManagementGroupId,
[switch]
$AzureDevOpsWikiAsCode, #deprecated - Based on environment variables the script will detect the code run platform
[switch]
$NoCsvExport,
[string]
[ValidateSet(';', ',')]$CsvDelimiter = ';',
[switch]
$CsvExportUseQuotesAsNeeded,
[string]
$OutputPath,
[switch]
$DoNotShowRoleAssignmentsUserData,
[switch]
$HierarchyMapOnly,
[string]
$HierarchyMapOnlyCustomDataJSON,
[Alias('NoASCSecureScore')]
[switch]
$NoMDfCSecureScore,
[switch]
$NoResourceProvidersDetailed,
[switch]
$NoResourceProvidersAtAll,
[int]
$LimitCriticalPercentage = 80,
[array]
$SubscriptionQuotaIdWhitelist = @('undefined'),
[array]
$SubscriptionIdWhitelist = @('undefined'),
[switch]
$NoPolicyComplianceStates,
[switch]
$NoResourceDiagnosticsPolicyLifecycle,
[switch]
$NoAADGroupsResolveMembers,
[int]
$AADServicePrincipalExpiryWarningDays = 14,
[switch]
$NoAzureConsumption, #obsolete
[switch]
$DoAzureConsumption,
[switch]
$DoAzureConsumptionPreviousMonth,
[int]
$AzureConsumptionPeriod = 2,
[switch]
$NoAzureConsumptionReportExportToCSV,
[switch]
$DoTranscript,
[int]
$HtmlTableRowsLimit = 20000, #HTML TenantSummary may become unresponsive depending on client device performance. A recommendation will be shown to use the CSV file instead of opening the TF table
[int]
$ThrottleLimit = 10,
[Alias('ExludedResourceTypesDiagnosticsCapable')]
[array]
$ExcludedResourceTypesDiagnosticsCapable = @('microsoft.web/certificates', 'microsoft.chaos/chaosexperiments'),
[switch]
$DoNotIncludeResourceGroupsOnPolicy,
[switch]
$DoNotIncludeResourceGroupsAndResourcesOnRBAC,
[Alias('AzureDevOpsWikiHierarchyDirection')]
[ValidateSet('TD', 'LR')][string]$MermaidDirection = 'TD',
[int]
$ChangeTrackingDays = 14,
[string]
$FileTimeStampFormat = 'yyyyMMdd_HHmmss',
[switch]
$NoJsonExport,
[switch]
$JsonExportExcludeResourceGroups,
[switch]
$JsonExportExcludeResources,
[switch]
$LargeTenant,
[switch]
$NoScopeInsights,
[int]
$AADGroupMembersLimit = 500,
[switch]
$PolicyAtScopeOnly,
[switch]
$RBACAtScopeOnly,
[switch]
$NoResources,
[switch]
$StatsOptOut,
[switch]
$NoSingleSubscriptionOutput,
[switch]
$ManagementGroupsOnly,
[string]
$DirectorySeparatorChar = [IO.Path]::DirectorySeparatorChar,
[switch]
$ShowMemoryUsage,
[int]
$CriticalMemoryUsage = 99,
[switch]
$DoPSRule,
[switch]
$PSRuleFailedOnly,
[string]
$PSRuleVersion,
[switch]
$NoPIMEligibility,
[switch]
$PIMEligibilityIgnoreScope,
[switch]
$NoPIMEligibilityIntegrationRoleAssignmentsAll,
[switch]
$NoALZPolicyVersionChecker,
[switch]
$NoDefinitionInsightsDedicatedHTML,
[switch]
$NoStorageAccountAccessAnalysis,
[array]
$StorageAccountAccessAnalysisSubscriptionTags = @('undefined'),
[array]
$StorageAccountAccessAnalysisStorageAccountTags = @('undefined'),
[switch]
$GitHubActionsOIDC,
[switch]
$NoNetwork,
[int]
$NetworkSubnetIPAddressUsageCriticalPercentage = 80,
[switch]
$ShowRunIdentifier,
#https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#role-based-access-control-limits
[int]
$LimitRBACCustomRoleDefinitionsTenant = 5000,
[int]
$LimitRBACRoleAssignmentsManagementGroup = 500,
#https://learn.microsoft.com/azure/governance/policy/overview#maximum-count-of-azure-policy-objects
[int]
$LimitPOLICYPolicyAssignmentsManagementGroup = 200,
[int]
$LimitPOLICYPolicyAssignmentsSubscription = 200,
[int]
$LimitPOLICYPolicyDefinitionsScopedManagementGroup = 500,
[int]
$LimitPOLICYPolicyDefinitionsScopedSubscription = 500,
[int]
$LimitPOLICYPolicySetAssignmentsManagementGroup = 200,
[int]
$LimitPOLICYPolicySetAssignmentsSubscription = 200,
[int]
$LimitPOLICYPolicySetDefinitionsScopedTenant = 2500,
[int]
$LimitPOLICYPolicySetDefinitionsScopedManagementGroup = 200,
[int]
$LimitPOLICYPolicySetDefinitionsScopedSubscription = 200,
#https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits
[int]
$LimitResourceGroups = 980,
[int]
$LimitTagsSubscription = 50,
[array]
$MSTenantIds = @('2f4a9838-26b7-47ee-be60-ccc1fdec5953', '33e01921-4d64-4f8c-a055-5bdaffd5e33d'),
[array]
$ValidPolicyEffects = @('addToNetworkGroup', 'append', 'audit', 'auditIfNotExists', 'deny', 'denyAction', 'deployIfNotExists', 'modify', 'manual', 'disabled', 'EnforceRegoPolicy', 'enforceSetting', 'mutate'),
[hashtable]
$APIMappingCloudEnvironment = @{
roleDefinitions = @{
AzureCloud = '2023-07-01-preview'
AzureUSGovernment = '2022-05-01-preview'
AzureChinaCloud = '2022-05-01-preview'
}
costManagementQuery = @{
AzureCloud = '2024-01-01'
AzureUSGovernment = '2023-09-01'
AzureChinaCloud = '2023-09-01'
}
securityPricings = @{
AzureCloud = '2024-01-01'
AzureUSGovernment = '2023-01-01'
AzureChinaCloud = '2023-01-01'
}
},
[array]
$SubscriptionQuotaIdsThatDoNotSupportCostManagementManagementGroupScopeQuery = @('CSP_2015-05-01') #https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/understand-cost-mgt-data#supported-microsoft-azure-offers
)
$Error.clear()
$ErrorActionPreference = 'Stop'
#removeNoise
$ProgressPreference = 'SilentlyContinue'
Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings 'true'
#start
$startAzGovViz = Get-Date
$startTime = Get-Date -Format 'dd-MMM-yyyy HH:mm:ss'
Write-Host "Start Azure Governance Visualizer (aka $Product) $($startTime) (#$($ProductVersion))"
if ($ManagementGroupId -match ' ') {
Write-Host "Provided Management Group ID: '$($ManagementGroupId)'" -ForegroundColor Yellow
Write-Host 'The Management Group ID may not contain spaces - provide the Management Group ID, not the displayName.' -ForegroundColor DarkRed
throw 'Management Group ID validation failed!'
}
#region Functions
. ".\$($ScriptPath)\functions\processMDfCCoverage.ps1"
. ".\$($ScriptPath)\functions\getPrivateEndpointCapableResourceTypes.ps1"
. ".\$($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\getConsumptionv2.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
#endregion Functions
$funcAddRowToTable = $function:addRowToTable.ToString()
$funcGetGroupmembers = $function:GetGroupmembers.ToString()
$funcResolveObjectIds = $function:ResolveObjectIds.ToString()
$funcNamingValidation = $function:NamingValidation.ToString()
$funcTestGuid = $function:testGuid.ToString()
$funcDetectPolicyEffect = $function:detectPolicyEffect.ToString()
$funcGetPolicyHash = $function:getPolicyHash.ToString()
if ($HierarchyMapOnly -and $HierarchyMapOnlyCustomDataJSON) {
processHierarchyMapOnlyCustomData
Write-Host 'Skipping PowerShell version check /Using custom data (`$HierarchyMapOnlyCustomDataJSON)'
}
else {
testPowerShellVersion
}
showMemoryUsage
$outputPathGiven = $OutputPath
setOutput
if ($DoTranscript) {
setTranscript
}
#region PSRule paused
if ($DoPSRule) {
Write-Host ''
Write-Host ' * * * CHANGE: PSRule for Azure * * *' -ForegroundColor Magenta
Write-Host 'PSRule integration has been paused'
Write-Host 'Azure Governance Visualizer leveraged the Invoke-PSRule cmdlet, but there are certain [resource types](https://github.com/Azure/PSRule.Rules.Azure/blob/ab0910359c1b9826d8134041d5ca997f6195fc58/src/PSRule.Rules.Azure/PSRule.Rules.Azure.psm1#L1582) where also child resources need to be queried to achieve full rule evaluation.'
$DoPSRule = $false
Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor Magenta
Write-Host ''
}
#endregion PSRule paused
#region verifyModules3rd
$modules = [System.Collections.ArrayList]@()
$null = $modules.Add([PSCustomObject]@{
ModuleName = 'AzAPICall'
ModuleVersion = $AzAPICallVersion
ModuleProductName = 'AzAPICall'
ModulePathPipeline = 'AzAPICallModule'
})
if ($DoPSRule) {
<#temporary workaround / PSRule/Azure DevOps Az.Resources module requirements
if ($env:SYSTEM_TEAMPROJECTID -and $env:BUILD_REPOSITORY_ID) {
$PSRuleVersion = '1.14.3'
Write-Host "Running in Azure DevOps; enforce PSRule version '$PSRuleVersion' (Az.Resources dependency on latest PSRule)"
}
#>
$null = $modules.Add([PSCustomObject]@{
ModuleName = 'PSRule.Rules.Azure'
ModuleVersion = $PSRuleVersion
ModuleProductName = 'PSRule'
ModulePathPipeline = 'PSRuleModule'
})
}
verifyModules3rd -modules $modules
#endregion verifyModules3rd
#Region initAZAPICall
Write-Host "Initialize 'AzAPICall'"
$parameters4AzAPICallModule = @{
DebugAzAPICall = $DebugAzAPICall
SubscriptionId4AzContext = $SubscriptionId4AzContext
TenantId4AzContext = $TenantId4AzContext
GithubRepository = $GithubRepository
SkipAzContextSubscriptionValidation = $AzAPICallSkipAzContextSubscriptionValidation
}
$azAPICallConf = initAzAPICall @parameters4AzAPICallModule
Write-Host " Initialize 'AzAPICall' succeeded" -ForegroundColor Green
Write-Host " Setting `$ignoreARMLocation to `$false" -ForegroundColor Yellow
$ignoreARMLocation = $false
if ($azApiCallConf['htParameters'].azureCloudEnvironment -ne 'AzureCloud') {
Write-Host " Non Public Cloud ($($azApiCallConf['htParameters'].azureCloudEnvironment)) -> Setting `$ignoreARMLocation to `$true" -ForegroundColor Yellow
$ignoreARMLocation = $true
}
if (-not $ignoreARMLocation) {
if ($azApiCallConf['htParameters'].ARMLocations.count -gt 0) {
Write-Host ''
Write-Host "Check if provided parameter value for -ARMLocation '$($ARMLocation)' is valid"
if ($azApiCallConf['htParameters'].ARMLocations -notcontains $ARMLocation) {
Write-Host " Parameter value for -ARMLocation '$($ARMLocation)' is not valid - please provide a valid ARMLocation" -ForegroundColor DarkRed
Write-Host " Valid ARMLocations: '$($azApiCallConf['htParameters'].ARMLocations -join ', ')'" -ForegroundColor Yellow
throw 'ARMLocation validation failed!'
}
else {
Write-Host " Parameter value for -ARMLocation '$($ARMLocation)' is valid" -ForegroundColor Green
}
}
else {
Write-Host ''
Write-Host "Skipping ARMLocation validation - no locations found in '`$azApiCallConf['htParameters'].ARMLocations'. (-SkipAzContextSubscriptionValidation = '$skipAzContextSubscriptionValidation')"
Write-Host " Setting `$ignoreARMLocation to `$true" -ForegroundColor Yellow
$ignoreARMLocation = $true
}
}
#EndRegion initAZAPICall
#region required AzAPICall version
if (-not ([System.Version]"$($azapicallConf['htParameters'].azAPICallModuleVersion)" -ge [System.Version]'1.2.4')) {
Write-Host ''
Write-Host 'Azure Governance Visualizer version '$ProductVersion' - AzAPICall PowerShell module version check failed -> https://aka.ms/AzAPICall; https://www.powershellgallery.com/packages/AzAPICall'
throw "This version of Azure Governance Visualizer '$ProductVersion' requires AzAPICall PowerShell module version '1.2.4' or greater"
}
else {
Write-Host ''
Write-Host "Azure Governance Visualizer version '$ProductVersion' - AzAPICall PowerShell module version requirement check succeeded: '1.2.4' or greater - current: '$($azapicallConf['htParameters'].azAPICallModuleVersion)' " -ForegroundColor Green
}
#endregion required AzAPICall version
checkAzGovVizVersion
#region promptNewAzGovVizVersionAvailable
if ($azGovVizNewerVersionAvailable) {
if (-not $azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) {
Write-Host ''
Write-Host " * * * This Azure Governance Visualizer version ($ProductVersion) is not up to date. Get the latest Azure Governance Visualizer version ($azGovVizVersionOnRepositoryFull)! * * *" -ForegroundColor Green
Write-Host 'Check the Azure Governance Visualizer history: https://github.com/Azure/Azure-Governance-Visualizer/blob/master/history.md'
Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor Green
Pause
}
}
#endregion promptNewAzGovVizVersionAvailable
if (-not $HierarchyMapOnly) {
handleCloudEnvironment
}
if (-not $HierarchyMapOnly) {
<# PSRule paused
#region recommendPSRule
if (-not $azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) {
if (-not $DoPSRule) {
Write-Host ''
Write-Host ' * * * RECOMMENDATION: PSRule for Azure * * *' -ForegroundColor Magenta
Write-Host "Parameter -DoPSRule == '$DoPSRule'"
Write-Host "'PSRule for Azure' based ouputs provide aggregated Microsoft Azure Well-Architected Framework (WAF) aligned resource analysis results including guidance for remediation."
Write-Host 'Consider running Azure Governance Visualizer with the parameter -DoPSRule (example: .\pwsh\AzGovVizParallel.ps1 -DoPSRule)'
Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor Magenta
Pause
}
}
#endregion recommendPSRule
#>
#region hintPIMEligibility
if ($azAPICallConf['htParameters'].accountType -eq 'User') {
if (-not $NoPIMEligibility) {
Write-Host ''
Write-Host ' * * * HINT: PIM (Privileged Identity Management) Eligibility reporting * * *' -ForegroundColor DarkBlue
Write-Host "Parameter -NoPIMEligibility == '$NoPIMEligibility'"
Write-Host "Executing principal accountType: '$($azAPICallConf['htParameters'].accountType)'"
Write-Host "PIM Eligibility reporting requires to execute the script as ServicePrincipal. API Permission 'PrivilegedAccess.Read.AzureResources' is required"
Write-Host "For this run we switch the parameter -NoPIMEligibility from '$NoPIMEligibility' to 'True'"
$NoPIMEligibility = $true
Write-Host "Parameter -NoPIMEligibility == '$NoPIMEligibility'"
Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor DarkBlue
Pause
}
}
#endregion hintPIMEligibility
}
#region delimiterOpposite
if ($CsvDelimiter -eq ';') {
$CsvDelimiterOpposite = ','
}
if ($CsvDelimiter -eq ',') {
$CsvDelimiterOpposite = ';'
}
#endregion delimiterOpposite
#region runDataCollection
#run
if ($HierarchyMapOnly -and $HierarchyMapOnlyCustomDataJSON) {
Write-Host 'Skipping Access validation /Using custom data (`$HierarchyMapOnlyCustomDataJSON)'
Write-Host 'Skipping addHtParameters /Using custom data (`$HierarchyMapOnlyCustomDataJSON)'
}
else {
addHtParameters
validateAccess
}
getFileNaming
Write-Host "Running Azure Governance Visualizer ($ProductVersion) for ManagementGroupId: '$ManagementGroupId'" -ForegroundColor Yellow
$newTable = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htMgDetails = @{}
$htSubDetails = @{}
if (-not $HierarchyMapOnly) {
#helper ht / collect results /save some time
$htCacheDefinitionsPolicy = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheDefinitionsPolicySet = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheDefinitionsRole = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheDefinitionsBlueprint = [System.Collections.Hashtable]::Synchronized(@{})
$htRoleAssignmentsPIM = [System.Collections.Hashtable]::Synchronized(@{})
$htPoliciesUsedInPolicySets = @{}
$htSubscriptionTags = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheAssignmentsPolicyOnResourceGroupsAndResources = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheAssignmentsRole = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheAssignmentsRBACOnResourceGroupsAndResources = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheAssignmentsBlueprint = [System.Collections.Hashtable]::Synchronized(@{})
$htCacheAssignmentsPolicy = [System.Collections.Hashtable]::Synchronized(@{})
$htCachePolicyComplianceMG = [System.Collections.Hashtable]::Synchronized(@{})
$htCachePolicyComplianceSUB = [System.Collections.Hashtable]::Synchronized(@{})
$htCachePolicyComplianceResponseTooLargeMG = [System.Collections.Hashtable]::Synchronized(@{})
$htCachePolicyComplianceResponseTooLargeSUB = [System.Collections.Hashtable]::Synchronized(@{})
$outOfScopeSubscriptions = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htOutOfScopeSubscriptions = @{}
if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) {
$htManagementGroupsCost = @{}
$htAzureConsumptionSubscriptions = @{}
$arrayConsumptionData = [System.Collections.ArrayList]@()
$arrayTotalCostSummary = @()
$azureConsumptionStartDate = ((Get-Date).AddDays( - ($($AzureConsumptionPeriod)))).ToString('yyyy-MM-dd')
$azureConsumptionEndDate = ((Get-Date).AddDays(-1)).ToString('yyyy-MM-dd')
if ($azAPICallConf['htParameters'].DoAzureConsumptionPreviousMonth -eq $true) {
$azureConsumptionStartDate = ((Get-Date).AddMonths(-1).AddDays( - $((Get-Date).Day) + 1)).ToString('yyyy-MM-dd')
$azureConsumptionEndDate = ((Get-Date).AddDays( - $((Get-Date).Day))).ToString('yyyy-MM-dd')
# Since the start and end date is calculated to start of day, we need to add one to get the full month
$AzureConsumptionPeriod = (New-TimeSpan -Start $azureConsumptionStartDate -End $azureConsumptionEndDate).Days + 1
}
}
$customDataCollectionDuration = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htResourceLocks = [System.Collections.Hashtable]::Synchronized(@{})
$htAllTagList = [System.Collections.Hashtable]::Synchronized(@{})
$htAllTagList.AllScopes = @{}
$htAllTagList.Subscription = @{}
$htAllTagList.ResourceGroup = @{}
$htAllTagList.Resource = @{}
$arrayTagList = [System.Collections.ArrayList]@()
$htSubscriptionTagList = [System.Collections.Hashtable]::Synchronized(@{})
$htPolicyAssignmentExemptions = [System.Collections.Hashtable]::Synchronized(@{})
$htUserTypesGuest = [System.Collections.Hashtable]::Synchronized(@{})
$resourcesAll = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$resourcesIdsAll = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$resourceGroupsAll = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htResourceProvidersAll = [System.Collections.Hashtable]::Synchronized(@{})
$arrayFeaturesAll = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htResourceTypesUniqueResource = [System.Collections.Hashtable]::Synchronized(@{})
$arrayDataCollectionProgressMg = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayDataCollectionProgressSub = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arraySubResourcesAddArrayDuration = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayDiagnosticSettingsMgSub = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htDiagnosticSettingsMgSub = @{}
$htDiagnosticSettingsMgSub.mg = @{}
$htDiagnosticSettingsMgSub.sub = @{}
$htMgAtScopePolicyAssignments = [System.Collections.Hashtable]::Synchronized(@{})
$htMgAtScopePoliciesScoped = [System.Collections.Hashtable]::Synchronized(@{})
$htMgAtScopeRoleAssignments = [System.Collections.Hashtable]::Synchronized(@{})
$htMgASCSecureScore = @{}
$htConsumptionExceptionLog = [System.Collections.Hashtable]::Synchronized(@{})
$htConsumptionExceptionLog.Mg = @{}
$htConsumptionExceptionLog.Sub = @{}
$htRoleAssignmentsFromAPIInheritancePrevention = [System.Collections.Hashtable]::Synchronized(@{})
$htNamingValidation = [System.Collections.Hashtable]::Synchronized(@{})
$htNamingValidation.PolicyAssignment = @{}
$htNamingValidation.Policy = @{}
$htNamingValidation.PolicySet = @{}
$htNamingValidation.Role = @{}
$htNamingValidation.Subscription = @{}
$htNamingValidation.ManagementGroup = @{}
$htPrincipals = [System.Collections.Hashtable]::Synchronized(@{})
$htServicePrincipals = [System.Collections.Hashtable]::Synchronized(@{})
$htDailySummary = @{}
$arrayDefenderPlans = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayDefenderPlansSubscriptionsSkipped = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htSecuritySettings = [System.Collections.Hashtable]::Synchronized(@{})
$arrayUserAssignedIdentities4Resources = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htSubscriptionsRoleAssignmentLimit = [System.Collections.Hashtable]::Synchronized(@{})
if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) {
$htMgASCSecureScore = @{}
}
$htManagedIdentityForPolicyAssignment = @{}
$htPolicyAssignmentManagedIdentity = @{}
$htManagedIdentityDisplayName = @{}
$htAppDetails = [System.Collections.Hashtable]::Synchronized(@{})
if (-not $NoAADGroupsResolveMembers) {
$htAADGroupsDetails = [System.Collections.Hashtable]::Synchronized(@{})
$htAADGroupsExeedingMemberLimit = [System.Collections.Hashtable]::Synchronized(@{})
$arrayGroupRoleAssignmentsOnServicePrincipals = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayGroupRequestResourceNotFound = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayProgressedAADGroups = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
}
if ($DoAzureConsumption) {
$allConsumptionData = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
}
$arrayPsRule = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayPSRuleTracking = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htClassicAdministrators = [System.Collections.Hashtable]::Synchronized(@{})
$arrayOrphanedResources = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayPIMEligible = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$alzPolicies = @{}
$alzPolicySets = @{}
$alzPolicyHashes = @{}
$alzPolicySetHashes = @{}
$htDoARMRoleAssignmentScheduleInstances = [System.Collections.Hashtable]::Synchronized(@{})
$htDoARMRoleAssignmentScheduleInstances.Do = $true
$storageAccounts = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayStorageAccountAnalysisResults = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htDefenderEmailContacts = [System.Collections.Hashtable]::Synchronized(@{})
$arrayVNets = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayPrivateEndPoints = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$arrayPrivateEndPointsFromResourceProperties = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htUnknownTenantsForSubscription = @{}
$htResourcePropertiesConvertfromJSONFailed = [System.Collections.Hashtable]::Synchronized(@{})
$htResourceProvidersRef = @{}
$htAvailablePrivateEndpointTypes = [System.Collections.Hashtable]::Synchronized(@{})
$arrayAdvisorScores = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$htHashesBuiltInPolicy = [System.Collections.Hashtable]::Synchronized(@{})
$arrayCustomBuiltInPolicyParity = [System.Collections.ArrayList]@()
$arrayRemediatable = [System.Collections.ArrayList]@()
}
if (-not $HierarchyMapOnly) {
if (-not $NoALZPolicyVersionChecker) {
switch ($azAPICallConf['checkContext'].Environment.Name) {
'Azurecloud' {
Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'"
processALZPolicyVersionChecker
}
'AzureChinaCloud' {
Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'"
processALZPolicyVersionChecker
}
'AzureUSGovernment' {
Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'"
processALZPolicyVersionChecker
}
Default {
Write-Host "'Azure Landing Zones (ALZ) Policy Version Checker' feature NOT supported for Cloud environment '$($azAPICallConf['checkContext'].Environment.Name)'"
Write-Host "Setting parameter -NoALZPolicyVersionChecker to 'true'"
$NoALZPolicyVersionChecker = $true
}
}
}
else {
#Write-Host "Skipping 'Azure Landing Zones (ALZ) Policy Version Checker' (parameter -NoALZPolicyVersionChecker = $NoALZPolicyVersionChecker)"
}
}
if ($HierarchyMapOnly -and $HierarchyMapOnlyCustomDataJSON) {
$script:hierarchyLevel = -1
$script:mgSubPathTopMg = "$ManagementGroupId"
$script:getMgParentId = "'$ManagementGroupId'"
$script:getMgParentName = 'Tenant Root'
$script:mermaidprnts = "'$getMgParentId',$getMgParentId"
}
else {
getSubscriptions
getEntities
showMemoryUsage
setBaseVariablesMG
}
if ($HierarchyMapOnly -and $HierarchyMapOnlyCustomDataJSON) {
}
else {
if ($azAPICallConf['htParameters'].accountType -eq 'User') {
getTenantDetails
}
getDefaultManagementGroup
}
runInfo
if (-not $HierarchyMapOnly) {
#getSubscriptions
detailSubscriptions
showMemoryUsage
if ($azAPICallConf['htParameters'].NoMDfCSecureScore -eq $false) {
getMDfCSecureScoreMG
}
if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) {
getConsumptionv2
}
getOrphanedResources
showMemoryUsage
cacheBuiltIn
showMemoryUsage
if ($subsToProcessInCustomDataCollection.count -eq 0) {
Write-Host '--- Info ---' -ForegroundColor Yellow
Write-Host '--- Seems this tenant has no subscriptions. Activating parameter -ManagementGroupsOnly' -ForegroundColor Yellow
$ManagementGroupsOnly = $true
$script:azAPICallConf['htParameters'].ManagementGroupsOnly = $true
}
if (-not $ManagementGroupsOnly) {
#region sanity check / AzContext has subscription
if (-not $azAPICallConf['checkcontext'].Subscription.Id) {
Write-Host '--- Sanity check ---' -ForegroundColor Yellow
Write-Host 'Current AzContext has no subscription:' -ForegroundColor Yellow
Write-Host ($azAPICallConf['checkcontext'] | Select-Object -ExcludeProperty Environment, ExtendedProperties | ConvertTo-Json -Depth 99)
if ($AzAPICallSkipAzContextSubscriptionValidation) {
Write-Host 'You have enabled the parameter -AzAPICallSkipAzContextSubscriptionValidation' -ForegroundColor Yellow
Write-Host "Please use the parameter -SubscriptionId4AzContext '<subscriptionId>'" -ForegroundColor Yellow
throw
}
else {
Write-Host 'You have NOT enabled the parameter -AzAPICallSkipAzContextSubscriptionValidation, but somehow reached this point in the script.' -ForegroundColor Yellow
Write-Host "Please use the parameter -SubscriptionId4AzContext '<subscriptionId>'" -ForegroundColor Yellow
throw
}
}
#endregion sanity check / AzContext has subscription
#region Getting Tenant Resource Providers
$startGetRPs = Get-Date
$currentTask = 'Getting Tenant Resource Providers'
Write-Host $currentTask
$uri = "$($azAPICallConf['azAPIEndpointUrls'].ARM)/providers?api-version=2021-04-01"
$method = 'GET'
$resourceProviders = AzAPICall -AzAPICallConfiguration $azAPICallConf -uri $uri -method $method -currentTask $currentTask -caller 'CustomDataCollection'
Write-Host " Returned $($resourceProviders.Count) Resource Provider namespaces"
foreach ($resourceProvider in $resourceProviders) {
foreach ($resourceProviderResourceType in $resourceProvider.resourceTypes) {
$APIs = $resourceProviderResourceType.apiVersions | Sort-Object -Descending
$htResourceProvidersRef.("$($resourceProvider.nameSpace)/$($resourceProviderResourceType.resourceType)") = @{}
$htResourceProvidersRef.("$($resourceProvider.nameSpace)/$($resourceProviderResourceType.resourceType)").APILatest = $APIs | Select-Object -First 1
$htResourceProvidersRef.("$($resourceProvider.nameSpace)/$($resourceProviderResourceType.resourceType)").APIs = $APIs
if (-not [string]::IsNullOrWhiteSpace($resourceProviderResourceType.defaultApiVersion)) {
$htResourceProvidersRef.("$($resourceProvider.nameSpace)/$($resourceProviderResourceType.resourceType)").APIDefault = $resourceProviderResourceType.defaultApiVersion
}
}
}
Write-Host " Created ht for $($htResourceProvidersRef.Keys.Count) Resource/sub types"
$endGetRPs = Get-Date
Write-Host "Getting Tenant Resource Providers duration: $((New-TimeSpan -Start $startGetRPs -End $endGetRPs).TotalMinutes) minutes ($((New-TimeSpan -Start $startGetRPs -End $endGetRPs).TotalSeconds) seconds)"
#endregion Getting Tenant Resource Providers
getPrivateEndpointCapableResourceTypes
}
Write-Host 'Collecting custom data'
$startDataCollection = Get-Date
processDataCollection -mgId $ManagementGroupId
if (-not $ManagementGroupsOnly) {
exportResourceLocks
}
getPolicyRemediation
if ($arrayAdvisorScores.Count -gt 0) {
if (-not $NoCsvExport) {
Write-Host "Exporting AdvisorScores CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_AdvisorScores.csv'"
$arrayAdvisorScores | Sort-Object -Property subscriptionName, subscriptionId, category | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_AdvisorScores.csv" -Delimiter "$csvDelimiter" -NoTypeInformation
}
}
showMemoryUsage
if (-not $NoPIMEligibility) {
getPIMEligible
showMemoryUsage
}
$endDataCollection = Get-Date
Write-Host "Collecting custom data duration: $((New-TimeSpan -Start $startDataCollection -End $endDataCollection).TotalMinutes) minutes ($((New-TimeSpan -Start $startDataCollection -End $endDataCollection).TotalSeconds) seconds)"
exportBaseCSV
}
else {
processHierarchyMapOnly
exportBaseCSV
}
prepareData
showMemoryUsage
if (-not $HierarchyMapOnly) {
$rbacBaseQuery = $newTable.where({ -not [String]::IsNullOrEmpty($_.RoleDefinitionName) } ) | Sort-Object -Property RoleIsCustom, RoleDefinitionName | Select-Object -Property Level, Role*, mg*, Subscription*
$roleAssignmentsUniqueById = $rbacBaseQuery | Sort-Object -Property RoleAssignmentId -Unique
if (-not $NoAADGroupsResolveMembers) {
processAADGroups
showMemoryUsage
}
processApplications
showMemoryUsage
processManagedIdentities
showMemoryUsage
if (-not $ManagementGroupsOnly) {
createTagList
showMemoryUsage
}
if ($azAPICallConf['htParameters'].NoStorageAccountAccessAnalysis -eq $false) {
if (-not $ManagementGroupsOnly) {
processStorageAccountAnalysis
showMemoryUsage
}
}
if ($azAPICallConf['htParameters'].NoResources -eq $false) {
if (-not $ManagementGroupsOnly) {
getResourceDiagnosticsCapability
}
showMemoryUsage
}
}
#endregion runDataCollection
#region createoutputs
#region BuildHTML
$startBuildHTML = Get-Date
Write-Host 'Building HTML'
$html = $null
#getFileNaming
if (-not $HierarchyMapOnly) {
#region preQueries
Write-Host ' Building preQueries'
$startPreQueries = Get-Date
#region Create Policy/Set helper hash table
Write-Host ' Create Policy/Set helper hash table'
$startHelperHt = Get-Date
$tenantAllPolicySets = ($htCacheDefinitionsPolicySet).Values
$tenantAllPolicySetsCount = ($tenantAllPolicySets).count
if ($tenantAllPolicySetsCount -gt 0) {
foreach ($policySet in $tenantAllPolicySets) {
$PolicySetPolicyIds = $policySet.PolicySetPolicyIds
foreach ($PolicySetPolicyId in $PolicySetPolicyIds) {
if ($policySet.LinkToAzAdvertizer) {
$hlperDisplayNameWithOrWithoutLinkToAzAdvertizer = "$($policySet.LinkToAzAdvertizer) ($($policySet.PolicyDefinitionId))"
}
else {
$hlperDisplayNameWithOrWithoutLinkToAzAdvertizer = "$($policySet.DisplayName) ($($policySet.PolicyDefinitionId))"
}
$hlper4CSVOutput = "$($policySet.DisplayName) ($($policySet.PolicyDefinitionId))"
if (-not $htPoliciesUsedInPolicySets.($PolicySetPolicyId)) {
$htPoliciesUsedInPolicySets.($PolicySetPolicyId) = @{}
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet = [array]$hlperDisplayNameWithOrWithoutLinkToAzAdvertizer
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV = [array]$hlper4CSVOutput
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly = [array]($policySet.PolicyDefinitionId)
$htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet = [System.Collections.ArrayList]@()
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet.Add($hlperDisplayNameWithOrWithoutLinkToAzAdvertizer)
$htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV = [System.Collections.ArrayList]@()
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV.Add($hlper4CSVOutput)
$htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly = [System.Collections.ArrayList]@()
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly.Add($policySet.PolicyDefinitionId)
}
else {
# $array = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet
# $array += $hlperDisplayNameWithOrWithoutLinkToAzAdvertizer
# $arrayCSV = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV
# $arrayCSV += $hlper4CSVOutput
# $arrayIdOnly = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly
# $arrayIdOnly += $policySet.PolicyDefinitionId
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet = $array
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV = $arrayCSV
# $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly = $arrayIdOnly
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet.Add($hlperDisplayNameWithOrWithoutLinkToAzAdvertizer)
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySet4CSV.Add($hlper4CSVOutput)
$null = $htPoliciesUsedInPolicySets.($PolicySetPolicyId).policySetIdOnly.Add($policySet.PolicyDefinitionId)
}
}
}
}
$endHelperHt = Get-Date
Write-Host " Create Policy/Set helper hash table duration: $((New-TimeSpan -Start $startHelperHt -End $endHelperHt).TotalSeconds) seconds"
#endregion Create Policy/Set helper hash table
#region PreQueriesPolicyRelated
$startPreQueriesPolicyRelated = Get-Date
if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) {
$policyBaseQuery = $newTable.where({ -not [String]::IsNullOrEmpty($_.PolicyVariant) } ) | Sort-Object -Property PolicyType, Policy | Select-Object -Property Level, Policy*, mg*, Subscription*
}
else {
$policyBaseQuery = $newTable.where({ -not [String]::IsNullOrEmpty($_.PolicyVariant) -and ($_.PolicyAssignmentScopeMgSubRg -eq 'Mg' -or $_.PolicyAssignmentScopeMgSubRg -eq 'Sub') } ) | Sort-Object -Property PolicyType, Policy | Select-Object -Property Level, Policy*, mg*, Subscription*
}
$policyBaseQuerySubscriptions = $policyBaseQuery.where({ -not [String]::IsNullOrEmpty($_.SubscriptionId) } )
$policyBaseQueryManagementGroups = $policyBaseQuery.where({ [String]::IsNullOrEmpty($_.SubscriptionId) } )
$policyPolicyBaseQueryScopeInsights = ($policyBaseQuery | Select-Object Mg*, Subscription*, PolicyAssignmentAtScopeCount, PolicySetAssignmentAtScopeCount, PolicyAndPolicySetAssignmentAtScopeCount, PolicyAssignmentLimit -Unique)
$policyBaseQueryUniqueAssignments = $policyBaseQuery | Sort-Object -Property PolicyAssignmentId -Unique | Select-Object -Property Policy*
$policyAssignmentsOrphaned = $policyBaseQuery.where({ $_.PolicyAvailability -eq 'na' } ) | Sort-Object -Property PolicyAssignmentId -Unique
$policyAssignmentsOrphanedCount = $policyAssignmentsOrphaned.Count
Write-Host " $policyAssignmentsOrphanedCount orphaned Policy assignments found"
$htPolicyWithAssignmentsBase = @{}
foreach ($policyAssignment in $policyBaseQueryUniqueAssignments) {
if ($policyAssignment.PolicyVariant -eq 'Policy') {
if (-not $htPolicyWithAssignmentsBase.($policyAssignment.PolicyDefinitionId)) {
$htPolicyWithAssignmentsBase.($policyAssignment.PolicyDefinitionId) = @{}
$htPolicyWithAssignmentsBase.($policyAssignment.PolicyDefinitionId).Assignments = [array]$policyAssignment.PolicyAssignmentId
}
else {
$usedInAssignments = $htPolicyWithAssignmentsBase.($policyAssignment.PolicyDefinitionId).Assignments
$usedInAssignments += $policyAssignment.PolicyAssignmentId
$htPolicyWithAssignmentsBase.($policyAssignment.PolicyDefinitionId).Assignments = $usedInAssignments
}
}
}
$policyPolicySetBaseQueryUniqueAssignments = $policyBaseQueryUniqueAssignments.where({ $_.PolicyVariant -eq 'PolicySet' } )
#$policyBaseQueryUniqueCustomDefinitions = ($policyBaseQuery.where({ $_.PolicyType -eq 'Custom' } )) | Select-Object PolicyVariant, PolicyDefinitionId -Unique
$policyBaseQueryUniqueCustomDefinitions = ($policyBaseQuery.where({ $_.PolicyType -eq 'Custom' } )) | Sort-Object -Property PolicyVariant, PolicyDefinitionId -Unique | Select-Object PolicyVariant, PolicyDefinitionId
$policyPolicyBaseQueryUniqueCustomDefinitions = ($policyBaseQueryUniqueCustomDefinitions.where({ $_.PolicyVariant -eq 'Policy' } )).PolicyDefinitionId
$policyPolicySetBaseQueryUniqueCustomDefinitions = ($policyBaseQueryUniqueCustomDefinitions.where({ $_.PolicyVariant -eq 'PolicySet' } )).PolicyDefinitionId
#region create array Policy definitions
$tenantAllPoliciesCount = (($htCacheDefinitionsPolicy).Values).count
$tenantBuiltInPolicies = (($htCacheDefinitionsPolicy).Values).where({ $_.Type -eq 'BuiltIn' } )
$tenantBuiltInPoliciesCount = ($tenantBuiltInPolicies).count
$tenantCustomPolicies = (($htCacheDefinitionsPolicy).Values).where({ $_.Type -eq 'Custom' } )
$tenantCustomPoliciesCount = ($tenantCustomPolicies).count
#roleDefinitions used in policyDefinitions
Write-Host 'Processing roleDefinitions used in policyDefinitions'
$startRoleDefinitionsUsedInPolicyDefinitions = Get-Date
$htRoleDefinitionIdsUsedInPolicy = @{}
foreach ($policyDefinitionId in $htCacheDefinitionsPolicy.Keys) {
if (-not [string]::IsNullOrWhiteSpace($htCacheDefinitionsPolicy.($policyDefinitionId).Json.properties.policyRule.then.details.roleDefinitionIds)) {
foreach ($roledefinitionId in $htCacheDefinitionsPolicy.($policyDefinitionId).Json.properties.policyRule.then.details.roleDefinitionIds) {
if (-not [string]::IsNullOrWhitespace($roledefinitionId)) {
$roleDefinitionIdGuid = $roledefinitionId -replace '.*/'
if (-not $htCacheDefinitionsRole.($roleDefinitionIdGuid)) {
Write-Host "Finding: policyDefinitionId '$($policyDefinitionId)' has unknown roleDefinitionId '$roledefinitionId' in policyRule.then.details.roleDefinitionIds" -ForegroundColor DarkRed
}
else {
if (-not $htRoleDefinitionIdsUsedInPolicy.($roleDefinitionIdGuid)) {
$htRoleDefinitionIdsUsedInPolicy.($roleDefinitionIdGuid) = [System.Collections.ArrayList]@()
}
try {
$null = $htRoleDefinitionIdsUsedInPolicy.($roleDefinitionIdGuid).Add($policyDefinitionId)
}
catch {
Write-Host "policyDefinitionId '$($policyDefinitionId)' JSON:"
$htCacheDefinitionsPolicy.($policyDefinitionId).Json | ConvertTo-Json -Depth 99
Write-Host '--->'
Throw "Failed: `$policyDefinitionId: '$($policyDefinitionId)' trying to add `$roledefinitionId: '$roledefinitionId' from policyRule.then.details.roleDefinitionIds to `$htRoleDefinitionIdsUsedInPolicy.(`$roledefinitionId).UsedInPolicies"
}
}
}
else {
Write-Host "Finding: policyDefinitionId '$($policyDefinitionId)' has empty roleDefinitionId in policyRule.then.details.roleDefinitionIds" -ForegroundColor DarkRed
}
}
}
}
Write-Host " $($htRoleDefinitionIdsUsedInPolicy.Keys.Count) roleDefinitions are used in policyDefinitions"
$endRoleDefinitionsUsedInPolicyDefinitions = Get-Date
Write-Host " roleDefinitions used in policyDefinitions duration: $((New-TimeSpan -Start $startRoleDefinitionsUsedInPolicyDefinitions -End $endRoleDefinitionsUsedInPolicyDefinitions).TotalSeconds) seconds"
#hashes for parity builtin/custom
Write-Host 'Processing Policy custom/built-In parity check'
$startPolicyCustomBuiltInParity = Get-Date
foreach ($customPolicy in $tenantCustomPolicies) {
$policyRuleHash = getPolicyHash -json ($customPolicy.Json.properties.policyRule | ConvertTo-Json -Depth 99)
if ($htHashesBuiltInPolicy.($policyRuleHash)) {
$null = $arrayCustomBuiltInPolicyParity.Add([PSCustomObject]@{
CustomPolicyName = $customPolicy.Name
CustomPolicyDisplayName = $customPolicy.DisplayName
CustomPolicyCategory = $customPolicy.Category
CustomPolicyId = $customPolicy.Id
MatchBuiltinPolicyCount = $htHashesBuiltInPolicy.($policyRuleHash).Policies.Count
BuiltInPolicyId = ($htHashesBuiltInPolicy.($policyRuleHash).Policies | Sort-Object) -join "$CsvDelimiterOpposite "
})
}
}
if ($arrayCustomBuiltInPolicyParity.Count -gt 0) {
Write-Host " $($arrayCustomBuiltInPolicyParity.Count) custom Policy definition(s) found that have parity with built-In Policy definition Policy rule"
if (-not $NoCsvExport) {
Write-Host " Exporting PolicyCustomBuiltInParity CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyCustomBuiltInParity.csv'"
$arrayCustomBuiltInPolicyParity | Sort-Object -Property CustomPolicyId | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyCustomBuiltInParity.csv" -Delimiter "$csvDelimiter" -NoTypeInformation
}
}
else {
Write-Host ' No custom Policy definition found that have parity with built-In Policy definition Policy rule'
}
$endPolicyCustomBuiltInParity = Get-Date
Write-Host " Policy custom/built-In parity check duration: $((New-TimeSpan -Start $startPolicyCustomBuiltInParity -End $endPolicyCustomBuiltInParity).TotalMinutes) minutes ($((New-TimeSpan -Start $startPolicyCustomBuiltInParity -End $endPolicyCustomBuiltInParity).TotalSeconds) seconds)"
#endregion create array Policy definitions
#region create array PolicySet definitions
$tenantBuiltInPolicySets = $tenantAllPolicySets.where({ $_.Type -eq 'Builtin' } )
$tenantBuiltInPolicySetsCount = ($tenantBuiltInPolicySets).count
$tenantCustomPolicySets = $tenantAllPolicySets.where({ $_.Type -eq 'Custom' } )
$tenantCustompolicySetsCount = ($tenantCustomPolicySets).count
#endregion create array PolicySet definitions
$endPreQueriesPolicyRelated = Get-Date
Write-Host " PreQueriesPolicyRelated duration: $((New-TimeSpan -Start $startPreQueriesPolicyRelated -End $endPreQueriesPolicyRelated).TotalSeconds) seconds"
#endregion PreQueriesPolicyRelated
#region PreQueriesRBACRelated
$startPreQueriesRBACRelated = Get-Date
$rbacBaseQueryArrayListNotGroupOwner = $rbacBaseQuery.where({ $_.RoleAssignmentIdentityObjectType -ne 'Group' -and $_.RoleDefinitionName -eq 'Owner' }) | Select-Object -Property mgid, SubscriptionId, RoleAssignmentId, RoleDefinitionName, RoleDefinitionId, RoleAssignmentIdentityObjectType, RoleAssignmentIdentityDisplayname, RoleAssignmentIdentitySignInName, RoleAssignmentIdentityObjectId, RoleAssignmentScopeType
$rbacBaseQueryArrayListNotGroupUserAccessAdministrator = $rbacBaseQuery.where({ $_.RoleAssignmentIdentityObjectType -ne 'Group' -and $_.RoleDefinitionName -eq 'User Access Administrator' }) | Select-Object -Property mgid, SubscriptionId, RoleAssignmentId, RoleDefinitionName, RoleDefinitionId, RoleAssignmentIdentityObjectType, RoleAssignmentIdentityDisplayname, RoleAssignmentIdentitySignInName, RoleAssignmentIdentityObjectId, RoleAssignmentScopeType
$roleAssignmentsForServicePrincipals = (($roleAssignmentsUniqueById.where({ $_.RoleAssignmentIdentityObjectType -eq 'ServicePrincipal' })))
$htRoleAssignmentsForServicePrincipals = @{}
foreach ($spWithRoleAssignment in $roleAssignmentsForServicePrincipals | Group-Object -Property RoleAssignmentIdentityObjectId) {
if (-not $htRoleAssignmentsForServicePrincipals.($spWithRoleAssignment.Name)) {
$htRoleAssignmentsForServicePrincipals.($spWithRoleAssignment.Name) = @{}
$htRoleAssignmentsForServicePrincipals.($spWithRoleAssignment.Name).RoleAssignments = $spWithRoleAssignment.group
}
}
#region assignmentRgRes
$htPoliciesWithAssignmentOnRgRes = @{}
foreach ($policyAssignmentRgRes in ($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values | Sort-Object -Property id -Unique) {
$hlperPolDefId = (($policyAssignmentRgRes.properties.policyDefinitionId).ToLower())
if (-not $htPoliciesWithAssignmentOnRgRes.($hlperPolDefId)) {
$pscustomObj = [System.Collections.ArrayList]@()
$null = $pscustomObj.Add([PSCustomObject]@{
PolicyAssignmentId = ($policyAssignmentRgRes.Id).ToLower()
PolicyAssignmentDisplayName = $policyAssignmentRgRes.properties.displayName
})
$htPoliciesWithAssignmentOnRgRes.($hlperPolDefId) = @{}
$htPoliciesWithAssignmentOnRgRes.($hlperPolDefId).Assignments = [array](($pscustomObj))
}
else {
$pscustomObj = [System.Collections.ArrayList]@()
$null = $pscustomObj.Add([PSCustomObject]@{
PolicyAssignmentId = ($policyAssignmentRgRes.Id).ToLower()
PolicyAssignmentDisplayName = $policyAssignmentRgRes.properties.displayName
})
$array = @()
$array += $htPoliciesWithAssignmentOnRgRes.($hlperPolDefId).Assignments
$array += (($pscustomObj))
$htPoliciesWithAssignmentOnRgRes.($hlperPolDefId).Assignments = $array
}
}
#endregion assignmentRgRes
$tenantAllRoles = ($htCacheDefinitionsRole).Values
$tenantAllRolesCount = ($tenantAllRoles).Count
$tenantCustomRoles = $tenantAllRoles.where({ $_.IsCustom -eq $True } )
$tenantCustomRolesCount = ($tenantCustomRoles).Count
$tenantAllRolesCanDoRoleAssignments = $tenantAllRoles.where({ $_.RoleCanDoRoleAssignments -eq $True } )
$tenantAllRolesCanDoRoleAssignmentsCount = $tenantAllRolesCanDoRoleAssignments.Count
$mgSubRoleAssignmentsArrayFromHTValues = ($htCacheAssignmentsRole).Values.Assignment
if ($azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) {
$rgResRoleAssignmentsArrayFromHTValues = ($htCacheAssignmentsRBACOnResourceGroupsAndResources).Values
}
$endPreQueriesRBACRelated = Get-Date
Write-Host " PreQueriesRBACRelated duration: $((New-TimeSpan -Start $startPreQueriesRBACRelated -End $endPreQueriesRBACRelated).TotalSeconds) seconds"
#endregion PreQueriesRBACRelated
$blueprintBaseQuery = ($newTable.where({ -not [String]::IsNullOrEmpty($_.BlueprintName) } )) | Select-Object mgid, SubscriptionId, Blueprint*
$mgsAndSubs = (($optimizedTableForPathQuery.where({ $_.mgId -ne '' -and $_.Level -ne '0' } )) | Sort-Object -Property MgId, SubscriptionId -Unique | Select-Object MgId, SubscriptionId)
#region PreQueriesDiagnosticsRelated
$startPreQueriesDiagnosticsRelated = Get-Date
$diagnosticSettingsMg = $arrayDiagnosticSettingsMgSub.where({ $_.Scope -eq 'Mg' -and $_.DiagnosticsPresent -eq 'true' })
$diagnosticSettingsMgCount = $diagnosticSettingsMg.Count
$diagnosticSettingsMgCategories = ($diagnosticSettingsMg.DiagnosticCategories | Group-Object -Property Category).Name
$diagnosticSettingsMgGrouped = $diagnosticSettingsMg | Group-Object -Property ScopeId
$diagnosticSettingsMgManagementGroupsCount = ($diagnosticSettingsMgGrouped | Measure-Object).Count
foreach ($entry in $diagnosticSettingsMgGrouped) {
$dsgrouped = $entry.group | Group-Object -Property DiagnosticSettingName
foreach ($ds in $dsgrouped) {
$targetTypegrouped = $ds.group | Group-Object -Property DiagnosticTargetType
foreach ($tt in $targetTypegrouped) {
if (-not ($htDiagnosticSettingsMgSub).mg.($entry.Name)) {
($htDiagnosticSettingsMgSub).mg.($entry.Name) = @{}
}
if (-not ($htDiagnosticSettingsMgSub).mg.($entry.Name).($ds.Name)) {
($htDiagnosticSettingsMgSub).mg.($entry.Name).($ds.Name) = @{}
}
if (-not ($htDiagnosticSettingsMgSub).mg.($entry.Name).($ds.Name).($tt.Name)) {
($htDiagnosticSettingsMgSub).mg.($entry.Name).($ds.Name).($tt.Name) = $tt.group
}
}
}
}
foreach ($mg in $htManagementGroupsMgPath.Values) {
foreach ($mgWithDiag in ($htDiagnosticSettingsMgSub).mg.keys) {
if ($mg.ParentNameChain -contains $mgWithDiag) {
foreach ($diagSet in ($htDiagnosticSettingsMgSub).mg.($mgWithDiag).keys) {
foreach ($tt in ($htDiagnosticSettingsMgSub).mg.($mgWithDiag).($diagset).keys) {
foreach ($tid in ($htDiagnosticSettingsMgSub).mg.($mgWithDiag).($diagset).($tt)) {
$null = $script:diagnosticSettingsMg.Add([PSCustomObject]@{
Scope = 'Mg'
ScopeName = $mg.displayName
ScopeId = $mg.Id
ScopeMgPath = $htManagementGroupsMgPath.($mg.Id).pathDelimited
DiagnosticsInheritedOrnot = $true
DiagnosticsInheritedFrom = $mgWithDiag
DiagnosticsPresent = 'true'
DiagnosticSettingName = $diagSet
DiagnosticTargetType = $tt
DiagnosticTargetId = $tid.DiagnosticTargetId
DiagnosticCategories = $tid.DiagnosticCategories
DiagnosticCategoriesHt = $tid.DiagnosticCategoriesHt
})
}
}
}
}
}
}
$mgsDiagnosticsApplicableCount = $diagnosticSettingsMg.Count
$arrayMgsWithoutDiagnostics = [System.Collections.ArrayList]@()
foreach ($mg in $htManagementGroupsMgPath.Values) {
if ($diagnosticSettingsMg.ScopeId -notcontains $mg.Id) {
$null = $arrayMgsWithoutDiagnostics.Add([PSCustomObject]@{
ScopeName = $mg.DisplayName
ScopeId = $mg.Id
ScopeMgPath = $mg.pathDelimited
})
}
}
$arrayMgsWithoutDiagnosticsCount = $arrayMgsWithoutDiagnostics.Count
$diagnosticSettingsSub = $arrayDiagnosticSettingsMgSub.where({ $_.Scope -eq 'Sub' -and $_.DiagnosticsPresent -eq 'true' })
$diagnosticSettingsSubCount = $diagnosticSettingsSub.Count
$diagnosticSettingsSubNoDiag = $arrayDiagnosticSettingsMgSub.where({ $_.Scope -eq 'Sub' -and $_.DiagnosticsPresent -eq 'false' })
$diagnosticSettingsSubNoDiagCount = $diagnosticSettingsSubNoDiag.Count
$diagnosticSettingsSubCategories = ($diagnosticSettingsSub.DiagnosticCategories | Group-Object -Property Category).Name
$diagnosticSettingsSubGrouped = $diagnosticSettingsSub | Group-Object -Property ScopeId
$diagnosticSettingsSubSubscriptionsCount = ($diagnosticSettingsSubGrouped | Measure-Object).Count
foreach ($entry in $diagnosticSettingsSubGrouped) {
$dsgrouped = $entry.group | Group-Object -Property DiagnosticSettingName
foreach ($ds in $dsgrouped) {
$targetTypegrouped = $ds.group | Group-Object -Property DiagnosticTargetType
foreach ($tt in $targetTypegrouped) {
if (-not ($htDiagnosticSettingsMgSub).sub.($entry.Name)) {
($htDiagnosticSettingsMgSub).sub.($entry.Name) = @{}
}
if (-not ($htDiagnosticSettingsMgSub).sub.($entry.Name).($ds.Name)) {
($htDiagnosticSettingsMgSub).sub.($entry.Name).($ds.Name) = @{}
}
if (-not ($htDiagnosticSettingsMgSub).sub.($entry.Name).($ds.Name).($tt.Name)) {
($htDiagnosticSettingsMgSub).sub.($entry.Name).($ds.Name).($tt.Name) = $tt.group
}
}
}
}
$endPreQueriesDiagnosticsRelated = Get-Date
Write-Host " PreQueriesDiagnosticsRelated duration: $((New-TimeSpan -Start $startPreQueriesDiagnosticsRelated -End $endPreQueriesDiagnosticsRelated).TotalSeconds) seconds"
#endregion PreQueriesDiagnosticsRelated
#region PreQueriesDefenderRelated
$startPreQueriesDefenderRelated = Get-Date
$defenderPlansGroupedBySub = $arrayDefenderPlans | Sort-Object -Property subscriptionName | Group-Object -Property subscriptionName, subscriptionId, subscriptionMgPath
$subsDefenderPlansCount = ($defenderPlansGroupedBySub | Measure-Object).Count
$defenderCapabilities = ($arrayDefenderPlans.defenderPlan | Sort-Object -Unique)
$defenderCapabilitiesCount = $defenderCapabilities.Count
$defenderPlansGroupedByPlan = $arrayDefenderPlans | Group-Object -Property defenderPlan, defenderPlanTier
$defenderPlansGroupedByPlanCount = ($defenderPlansGroupedByPlan | Measure-Object).Count
if ($defenderPlansGroupedByPlan.Name -contains 'ContainerRegistry, Standard' -or $defenderPlansGroupedByPlan.Name -contains 'KubernetesService, Standard') {
if ($defenderPlansGroupedByPlan.Name -contains 'ContainerRegistry, Standard') {
$defenderPlanDeprecatedContainerRegistry = $true
}
if ($defenderPlansGroupedByPlan.Name -contains 'KubernetesService, Standard') {
$defenderPlanDeprecatedKubernetesService = $true
}
}
$endPreQueriesDefenderRelated = Get-Date
Write-Host " PreQueriesDefenderRelated duration: $((New-TimeSpan -Start $startPreQueriesDefenderRelated -End $endPreQueriesDefenderRelated).TotalSeconds) seconds"
#endregion PreQueriesDefenderRelated
$endPreQueries = Get-Date
Write-Host " Pre Queries duration: $((New-TimeSpan -Start $startPreQueries -End $endPreQueries).TotalMinutes) minutes ($((New-TimeSpan -Start $startPreQueries -End $endPreQueries).TotalSeconds) seconds)"
showMemoryUsage
#endregion preQueries
#region summarizeDataCollectionResults
$startSummarizeDataCollectionResults = Get-Date
Write-Host 'Summary data collection'
#$mgsDetails = ($optimizedTableForPathQueryMg | Select-Object Level, MgId -Unique)
$mgsDetails = ($optimizedTableForPathQueryMg | Sort-Object -Property Level, MgId -Unique | Select-Object Level, MgId)
$mgDepth = ($mgsDetails.Level | Measure-Object -Maximum).Maximum
$totalMgCount = ($mgsDetails).count
$totalSubCount = ($optimizedTableForPathQuerySub).count
$totalSubOutOfScopeCount = ($outOfScopeSubscriptions).count
$totalSubIncludedAndExcludedCount = $totalSubCount + $totalSubOutOfScopeCount
$totalResourceCount = $($resourcesIdsAll.Count)
$totalPolicyAssignmentsCount = (($htCacheAssignmentsPolicy).keys).count
$policyAssignmentsMg = (($htCacheAssignmentsPolicy).Values.where({ $_.AssignmentScopeMgSubRg -eq 'Mg' } ))
$totalPolicyAssignmentsCountMg = $policyAssignmentsMg.Count
$totalPolicyAssignmentsCountSub = (($htCacheAssignmentsPolicy).Values.where({ $_.AssignmentScopeMgSubRg -eq 'Sub' } )).count
if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsOnPolicy) {
$totalPolicyAssignmentsCountRg = (($htCacheAssignmentsPolicy).Values.where({ $_.AssignmentScopeMgSubRg -eq 'Rg' -or $_.AssignmentScopeMgSubRg -eq 'Res' } )).count
}
else {
$totalPolicyAssignmentsCountRg = (($htCacheAssignmentsPolicyOnResourceGroupsAndResources).values).count
$totalPolicyAssignmentsCount = $totalPolicyAssignmentsCount + $totalPolicyAssignmentsCountRg
}
$totalRoleAssignmentsCount = (($htCacheAssignmentsRole).keys).count
$totalRoleAssignmentsCountTen = (($htCacheAssignmentsRole).keys.where({ ($htCacheAssignmentsRole).($_).AssignmentScopeTenMgSubRgRes -eq 'Tenant' } )).count
$totalRoleAssignmentsCountMG = (($htCacheAssignmentsRole).keys.where({ ($htCacheAssignmentsRole).($_).AssignmentScopeTenMgSubRgRes -eq 'MG' } )).count
$totalRoleAssignmentsCountSub = (($htCacheAssignmentsRole).keys.where({ ($htCacheAssignmentsRole).($_).AssignmentScopeTenMgSubRgRes -eq 'Sub' } )).count
if (-not $azAPICallConf['htParameters'].DoNotIncludeResourceGroupsAndResourcesOnRBAC) {
$totalRoleAssignmentsCountRG = (($htCacheAssignmentsRole).keys.where({ ($htCacheAssignmentsRole).($_).AssignmentScopeTenMgSubRgRes -eq 'RG' } )).count
$totalRoleAssignmentsCountRes = (($htCacheAssignmentsRole).keys.where({ ($htCacheAssignmentsRole).($_).AssignmentScopeTenMgSubRgRes -eq 'Res' } )).count
$totalRoleAssignmentsResourceGroupsAndResourcesCount = $totalRoleAssignmentsCountRG + $totalRoleAssignmentsCountRes
}
else {
$totalRoleAssignmentsResourceGroupsAndResourcesCount = (($htCacheAssignmentsRBACOnResourceGroupsAndResources).values).count
$totalRoleAssignmentsCount = $totalRoleAssignmentsCount + $totalRoleAssignmentsResourceGroupsAndResourcesCount
}
$totalRoleDefinitionsCustomCount = ((($htCacheDefinitionsRole).keys.where({ ($htCacheDefinitionsRole).($_).IsCustom -eq $True } ))).count
$totalBlueprintDefinitionsCount = ((($htCacheDefinitionsBlueprint).keys)).count
$totalBlueprintAssignmentsCount = (($htCacheAssignmentsBlueprint).keys).count
$totalResourceTypesCount = ($resourceTypesDiagnosticsArray).Count
Write-Host " Total Management Groups: $totalMgCount (depth $mgDepth)"
$htDailySummary.'ManagementGroups' = $totalMgCount
Write-Host " Total Subscriptions: $totalSubIncludedAndExcludedCount ($totalSubCount included; $totalSubOutOfScopeCount out-of-scope)"
$htDailySummary.'Subscriptions' = $totalSubCount
$subscriptionsGroupedByQuotaId = $optimizedTableForPathQuerySub | Group-Object -Property SubscriptionQuotaId
if ($subscriptionsGroupedByQuotaId.Count -gt 0) {
foreach ($quotaId in $subscriptionsGroupedByQuotaId) {
$htDailySummary."Subscriptions_$($quotaId.Name)" = $quotaId.Count
}
}
$htDailySummary.'SubscriptionsOutOfScope' = $totalSubOutOfScopeCount
Write-Host " Total BuiltIn Policy definitions: $tenantBuiltInPoliciesCount"
$htDailySummary.'PolicyDefinitionsBuiltIn' = $tenantBuiltInPoliciesCount
Write-Host " Total Custom Policy definitions: $tenantCustomPoliciesCount"
$htDailySummary.'PolicyDefinitionsCustom' = $tenantCustomPoliciesCount
Write-Host " Total BuiltIn PolicySet definitions: $tenantBuiltInPolicySetsCount"
$htDailySummary.'PolicySetDefinitionsBuiltIn' = $tenantBuiltInPolicySetsCount
Write-Host " Total Custom PolicySet definitions: $tenantCustompolicySetsCount"
$htDailySummary.'PolicySetDefinitionsCustom' = $tenantCustompolicySetsCount
Write-Host " Total Policy assignments: $($totalPolicyAssignmentsCount)"
$htDailySummary.'PolicyAssignments' = $totalPolicyAssignmentsCount
Write-Host " Total Policy assignments ManagementGroups $($totalPolicyAssignmentsCountMg)"
$htDailySummary.'PolicyAssignments_ManagementGroups' = $totalPolicyAssignmentsCountMg
Write-Host " Total Policy assignments Subscriptions $($totalPolicyAssignmentsCountSub)"
$htDailySummary.'PolicyAssignments_Subscriptions' = $totalPolicyAssignmentsCountSub
Write-Host " Total Policy assignments ResourceGroups: $($totalPolicyAssignmentsCountRg)"
$htDailySummary.'PolicyAssignments_ResourceGroups' = $totalPolicyAssignmentsCountRg
Write-Host " Total Custom Role definitions: $totalRoleDefinitionsCustomCount"
$htDailySummary.'RoleDefinitionsCustom' = $totalRoleDefinitionsCustomCount
Write-Host " Total Role assignments: $totalRoleAssignmentsCount"
$htDailySummary.'TotalRoleAssignments' = $totalRoleAssignmentsCount
Write-Host " Total Role assignments (Tenant): $totalRoleAssignmentsCountTen"
$htDailySummary.'TotalRoleAssignments_Tenant' = $totalRoleAssignmentsCountTen
Write-Host " Total Role assignments (ManagementGroups): $totalRoleAssignmentsCountMG"
$htDailySummary.'TotalRoleAssignments_ManagementGroups' = $totalRoleAssignmentsCountMG
Write-Host " Total Role assignments (Subscriptions): $totalRoleAssignmentsCountSub"
$htDailySummary.'TotalRoleAssignments_Subscriptions' = $totalRoleAssignmentsCountSub
Write-Host " Total Role assignments (ResourceGroups and Resources): $totalRoleAssignmentsResourceGroupsAndResourcesCount"
$htDailySummary.'TotalRoleAssignments_RgRes' = $totalRoleAssignmentsResourceGroupsAndResourcesCount
Write-Host " Total Blueprint definitions: $totalBlueprintDefinitionsCount"
$htDailySummary.'Blueprints' = $totalBlueprintDefinitionsCount
Write-Host " Total Blueprint assignments: $totalBlueprintAssignmentsCount"
$htDailySummary.'BlueprintAssignments' = $totalBlueprintAssignmentsCount
Write-Host " Total Resources: $totalResourceCount"
$htDailySummary.'Resources' = $totalResourceCount
Write-Host " Total Resource Types: $totalResourceTypesCount"
$htDailySummary.'ResourceTypes' = $totalResourceTypesCount
$rbacUnique = $rbacAll | Sort-Object -Property RoleAssignmentId -Unique
$rbacUniqueObjectIds = $rbacUnique | Sort-Object -Property ObjectId -Unique
$rbacUniqueObjectIdsNonPIM = $rbacUnique.where({ $_.RoleAssignmentPIMRelated -eq $false } ) | Sort-Object -Property ObjectId -Unique
$rbacUniqueObjectIdsPIM = $rbacUnique.where({ $_.RoleAssignmentPIMRelated -eq $true } ) | Sort-Object -Property ObjectId -Unique
if ($rbacUniqueObjectIds.Count -gt 0) {
$rbacUniqueObjectIdsGrouped = $rbacUniqueObjectIds | Group-Object -Property ObjectType
foreach ($principalType in $rbacUniqueObjectIdsGrouped) {
$htDailySummary."TotalUniquePrincipalWithPermission_$($principalType.Name)" = $principalType.Count
}
$htDailySummary.'TotalUniquePrincipalWithPermission_SP' = $rbacUniqueObjectIds.where({ $_.ObjectType -like 'SP*' } ).count
$htDailySummary.'TotalUniquePrincipalWithPermission_User' = $rbacUniqueObjectIds.where({ $_.ObjectType -like 'User*' } ).count
}
if ($rbacUniqueObjectIdsNonPIM.Count -gt 0) {
$rbacUniqueObjectIdsNonPIMGrouped = $rbacUniqueObjectIdsNonPIM | Group-Object -Property ObjectType
foreach ($principalType in $rbacUniqueObjectIdsNonPIMGrouped) {
$htDailySummary."TotalUniquePrincipalWithPermissionStatic_$($principalType.Name)" = $principalType.Count
}
$htDailySummary.'TotalUniquePrincipalWithPermissionStatic_SP' = $rbacUniqueObjectIdsNonPIM.where({ $_.ObjectType -like 'SP*' } ).count
$htDailySummary.'TotalUniquePrincipalWithPermissionStatic_User' = $rbacUniqueObjectIdsNonPIM.where({ $_.ObjectType -like 'User*' } ).count
}
if ($rbacUniqueObjectIdsPIM.Count -gt 0) {
$rbacUniqueObjectIdsPIMGrouped = $rbacUniqueObjectIdsPIM | Group-Object -Property ObjectType
foreach ($principalType in $rbacUniqueObjectIdsPIMGrouped) {
$htDailySummary."TotalUniquePrincipalWithPermissionPIM_$($principalType.Name)" = $principalType.Count
}
$htDailySummary.'TotalUniquePrincipalWithPermissionPIM_SP' = $rbacUniqueObjectIdsPIM.where({ $_.ObjectType -like 'SP*' } ).count
$htDailySummary.'TotalUniquePrincipalWithPermissionPIM_User' = $rbacUniqueObjectIdsPIM.where({ $_.ObjectType -like 'User*' } ).count
}
# if ($arrayAdvisorScores.Count -gt 0) {
# $arrayAdvisorScoresGroupedByCategory = $arrayAdvisorScores | Group-Object -Property category
# foreach ($entry in $arrayAdvisorScoresGroupedByCategory) {
# $htDailySummary."Advisor_$($entry.Name)" = ($entry.Group.Score | Measure-Object -Sum).Sum / $entry.Group.Count
# }
# }
if ($htMgASCSecureScore.Keys.Count -gt 0) {
foreach ($mgASCSecureScore in $htMgASCSecureScore.Keys) {
$htDailySummary."MDfCSecureScore_$($mgASCSecureScore)" = $htMgASCSecureScore.($mgASCSecureScore).SecureScore
}
}
$endSummarizeDataCollectionResults = Get-Date
Write-Host " Summary data collection duration: $((New-TimeSpan -Start $startSummarizeDataCollectionResults -End $endSummarizeDataCollectionResults).TotalSeconds) seconds"
showMemoryUsage
#endregion summarizeDataCollectionResults
}
$html = @"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>Azure Governance Visualizer aka AzGovViz</title>
<script type="text/javascript">
var link = document.createElement( "link" );
rand = Math.floor(Math.random() * 99999);
link.href = "https://www.azadvertizer.net/azgovvizv4/css/azgovvizversion.css?rnd=" + rand;
link.type = "text/css";
link.rel = "stylesheet";
link.media = "screen,print";
document.getElementsByTagName( "head" )[0].appendChild( link );
</script>
<link rel="stylesheet" type="text/css" href="https://www.azadvertizer.net/azgovvizv4/css/azgovvizmain_004_052.css">
<!--<script src="https://www.azadvertizer.net/azgovvizv4/js/jquery-3.6.0.min.js"></script>-->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<!--<script src="https://www.azadvertizer.net/azgovvizv4/js/jquery-ui-1.13.0.min.js"></script>-->
<script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js" integrity="sha256-sw0iNNXmOJbQhYFuC9OF2kOlD5KQKe1y5lfBn4C9Sjg=" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://www.azadvertizer.net/azgovvizv4/js/highlight_v004_002.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/fontawesome-0c0b5cbde8.js"></script>
<!--<script src="https://www.azadvertizer.net/azgovvizv4/tablefilter/tablefilter.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tablefilter/0.7.3/tablefilter.js" integrity="sha512-HDzCUKAvjWV4XogiGFmF59gZGeNUd7X/peY+4zRQQRlqjwYngxA2haFABelr9AEhnnq65CPM/yIgdi2ffpXxcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tablefilter/0.7.3/tf-1-2aa33b10e0e549020c12.min.js" integrity="sha512-KEstgdRK/uzfufHDzCmFIcjBN20mv8joRQdiQR71s0V+sP3j3mmgedDmK8i12INhG4wEWoDJHSKOpGxpAZ48qw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://www.azadvertizer.net/azgovvizv4/css/highlight-10.5.0.min.css">
<!--<script src="https://www.azadvertizer.net/azgovvizv4/js/highlight-10.5.0.min.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js" integrity="sha512-9GIHU4rPKUMvNOHFOer5Zm2zHnZOjayOO3lZpokhhCtgt8FNlNiW/bb7kl0R5ZXfCDVPcQ8S4oBdNs92p5Nm2w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>hljs.initHighlightingOnLoad();</script>
<link rel="stylesheet" type="text/css" href="https://www.azadvertizer.net/azgovvizv4/css/jsonviewer_v01.css">
<script type="text/javascript" src="https://www.azadvertizer.net/azgovvizv4/js/jsonviewer_v02.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/dom-to-image.min.js"></script>
<script>
`$(window).on('load', function () {
// Animate loader off screen
`$(".se-pre-con").fadeOut("slow");;
});
</script>
<script>
// Quick and simple export target #table_id into a csv
function download_table_as_csv_semicolon(table_id) {
// Select rows from table_id
var rows = document.querySelectorAll('table#' + table_id + ' tr');
// Construct csv
var csv = [];
if (window.helpertfConfig4TenantSummary_roleAssignmentsAll !== 1){
for (var i = 0; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll('td, th');
for (var j = 0; j < cols.length; j++) {
// Clean innertext to remove multiple spaces and jumpline (break csv)
var data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
// Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
data = data.replace(/"/g, '""');
// Push escaped string
row.push('"' + data + '"');
}
csv.push(row.join(';'));
}
}
else{
for (var i = 1; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll('td, th');
for (var j = 0; j < cols.length; j++) {
// Clean innertext to remove multiple spaces and jumpline (break csv)
var data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
// Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
data = data.replace(/"/g, '""');
// Push escaped string
row.push('"' + data + '"');
}
csv.push(row.join(';'));
}
}
var csv_string = csv.join('\n');
// Download it
var filename = 'export_' + table_id + '_' + new Date().toLocaleDateString('en-CA') + '.csv';
var link = document.createElement('a');
link.style.display = 'none';
link.setAttribute('target', '_blank');
link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
<script>
// Quick and simple export target #table_id into a csv
function download_table_as_csv_comma(table_id) {
// Select rows from table_id
var rows = document.querySelectorAll('table#' + table_id + ' tr');
// Construct csv
var csv = [];
if (window.helpertfConfig4TenantSummary_roleAssignmentsAll !== 1){
for (var i = 0; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll('td, th');
for (var j = 0; j < cols.length; j++) {
// Clean innertext to remove multiple spaces and jumpline (break csv)
var data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
// Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
data = data.replace(/"/g, '""');
// Push escaped string
row.push('"' + data + '"');
}
csv.push(row.join(','));
}
}
else{
for (var i = 1; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll('td, th');
for (var j = 0; j < cols.length; j++) {
// Clean innertext to remove multiple spaces and jumpline (break csv)
var data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
// Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
data = data.replace(/"/g, '""');
// Push escaped string
row.push('"' + data + '"');
}
csv.push(row.join(','));
}
}
var csv_string = csv.join('\n');
// Download it
var filename = 'export_' + table_id + '_' + new Date().toLocaleDateString('en-CA') + '.csv';
var link = document.createElement('a');
link.style.display = 'none';
link.setAttribute('target', '_blank');
link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
</head>
"@
if (-not $HierarchyMapOnly) {
if (-not $NoDefinitionInsightsDedicatedHTML) {
$htmlDefinitionInsightsDedicatedStart = $html
$htmlDefinitionInsightsDedicatedStart += @'
<body>
<div class="se-pre-con"></div>
<div class="hierprnt" id="hierprnt">
<div class="definitioninsightsprnt" id="definitioninsightsprnt" style="width:100%;height:100%;overflow-y:auto;resize: none;">
<div class="definitioninsights" id="definitioninsights"><p class="pbordered">DefinitionInsights</p>
'@
$htmlDefinitionInsightsDedicatedEnd = @"
</div><!--definitionInsights-->
</div><!--definitionInsightsprnt-->
</div>
<div class="footer">
<div class="VersionDiv VersionLatest"></div>
<div class="VersionDiv VersionThis"></div>
<div class="VersionAlert"></div>
</div>
<script src="https://www.azadvertizer.net/azgovvizv4/js/toggle_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/collapsetable_v004_001.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/fitty_v004_001.min.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/version_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/autocorrectOff_v004_001.js"></script>
<script>
fitty('#fitme', {
minSize: 7,
maxSize: 10
});
</script>
<script>
`$("#getImage").on('click', function () {
element = document.getElementById('saveAsImageArea')
var images = element.getElementsByTagName('img');
var l = images.length;
for (var i = 0; i < l; i++) {
images[0].parentNode.removeChild(images[0]);
}
var scale = 3;
domtoimage.toPng(element, { quality: 0.95 , width: element.clientWidth * scale,
height: element.clientHeight * scale,
style: {
transform: 'scale('+scale+')',
transformOrigin: 'top left'
}})
.then(function (dataUrl) {
var link = document.createElement('a');
link.download = '$($fileName).png';
link.href = dataUrl;
link.click();
});
})
</script>
</body>
</html>
"@
}
if (-not $NoSingleSubscriptionOutput) {
if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) {
$HTMLPath = "HTML-Subscriptions_$($ManagementGroupId)"
if (Test-Path -LiteralPath "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)") {
Write-Host ' Cleaning old state (Pipeline only)'
Remove-Item -Recurse -Force "$($outputPath)$($DirectorySeparatorChar)$($HTMLPath)"
}
}
else {
$HTMLPath = "HTML-Subscriptions_$($ManagementGroupId)_$($fileTimestamp)"
Write-Host " Creating new state ($($HTMLPath)) (local only))"
}
$null = New-Item -Name $HTMLPath -ItemType directory -Path $outputPath
$htmlSubscriptionOnlyStart = $html
$htmlSubscriptionOnlyStart += @'
<body>
<div class="se-pre-con"></div>
<div class="hierprnt" id="hierprnt">
<div class="hierarchyTables" id="hierarchyTables">
<p class="pbordered">ScopeInsights</p>
<table class="subTable">
'@
$htmlSubscriptionOnlyEnd = @"
</table>
</div>
</div>
<div class="footer">
<div class="VersionDiv VersionLatest"></div>
<div class="VersionDiv VersionThis"></div>
<div class="VersionAlert"></div>
</div>
<script src="https://www.azadvertizer.net/azgovvizv4/js/toggle_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/collapsetable_v004_001.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/fitty_v004_001.min.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/version_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/autocorrectOff_v004_001.js"></script>
<script>
fitty('#fitme', {
minSize: 7,
maxSize: 10
});
</script>
<script>
`$("#getImage").on('click', function () {
element = document.getElementById('saveAsImageArea')
var images = element.getElementsByTagName('img');
var l = images.length;
for (var i = 0; i < l; i++) {
images[0].parentNode.removeChild(images[0]);
}
var scale = 3;
domtoimage.toPng(element, { quality: 0.95 , width: element.clientWidth * scale,
height: element.clientHeight * scale,
style: {
transform: 'scale('+scale+')',
transformOrigin: 'top left'
}})
.then(function (dataUrl) {
var link = document.createElement('a');
link.download = '$($fileName).png';
link.href = dataUrl;
link.click();
});
})
</script>
</body>
</html>
"@
}
$htmlShowHideScopeInfo =
@"
<p>
<button id="showHideScopeInfo">Hide<br>ScopeInfo</button><br>
<a id="getImage" href="#"><button>save image</button></a>
<script>
`$("#showHideScopeInfo").click(function() {
if (`$(this).html() == "Hide<br>ScopeInfo") {
`$(this).html("Show<br>ScopeInfo");
jQuery('.extraInfoContent').hide();
} else {
`$(this).html("Hide<br>ScopeInfo");
jQuery('.extraInfoContent').show();
};
});
</script>
</p>
"@
}
else {
$htmlShowHideScopeInfo = '<p><a id="getImage" href="#"><button>save image</button></a></p>'
}
$html += @"
<body>
<div class="se-pre-con"></div>
<div class="tree">
<div class="hierarchyTree" id="hierarchyTree">
<div class="treeFeatureSel">
<p class="pbordered pborderedspecial">HierarchyMap</p>
$($htmlShowHideScopeInfo)
</div>
"@
$html += @'
<ul>
<div id="saveAsImageArea">
<li id="first" style="background-color:white">
'@
if ($tenantDisplayName) {
$tenantDetailsDisplay = "$tenantDisplayName<br>$tenantDefaultDomain<br>$($azAPICallConf['checkContext'].Tenant.Id)"
}
else {
if ($HierarchyMapOnly -and $HierarchyMapOnlyCustomDataJSON) {
$tenantDetailsDisplay = $ManagementGroupId
}
else {
$tenantDetailsDisplay = "$($azAPICallConf['checkContext'].Tenant.Id)"
}
}
$tenantRoleAssignmentCount = 0
if ($htMgAtScopeRoleAssignments.tenantLevelRoleAssignments) {
$tenantRoleAssignmentCount = $htMgAtScopeRoleAssignments.tenantLevelRoleAssignments.AssignmentsCount
}
$html += @'
<a class="tenant">
<div class="main">
<div class="extraInfo">
<div class="extraInfoContent">
<div class="extraInfoPlchldr"></div>
'@
$html += @'
</div>
<div class="treeMgLogo">
<img class="imgTreeLogoTenant" src="https://www.azadvertizer.net/azgovvizv4/icon/Azurev2.png">
</div>
<div class="extraInfoContent">
'@
if ($tenantRoleAssignmentCount -gt 0) {
$html += @"
<div class="extraInfoRoleAss">
<abbr class="abbrTree" title="$($tenantRoleAssignmentCount) Role assignments">$($tenantRoleAssignmentCount)</abbr>
</div>
"@
}
else {
$html += @'
<div class="extraInfoPlchldr"></div>
'@
}
$html += @"
</div>
</div>
<div class="fitme" id="fitme">$($tenantDetailsDisplay)
</div>
</div>
</a>
"@
if ($getMgParentName -eq 'Tenant Root') {
$html += @'
<ul>
'@
}
else {
if ($parentMgNamex -eq $parentMgIdx) {
$mgNameAndOrId = $parentMgNamex
}
else {
$mgNameAndOrId = "$parentMgNamex<br><i>$parentMgIdx</i>"
}
if ($tenantDisplayName) {
$tenantDetailsDisplay = "$tenantDisplayName<br>$tenantDefaultDomain<br>"
}
else {
$tenantDetailsDisplay = ''
}
$policiesMgScoped = ($htCacheDefinitionsPolicy).values.where({ $_.ScopeMgSub -eq 'Mg' })
$policySetsMgScoped = ($htCacheDefinitionsPolicySet).values.where({ $_.ScopeMgSub -eq 'Mg' })
$roleAssignmentsMg = (($htCacheAssignmentsRole).values.where({ $_.AssignmentScopeTenMgSubRgRes -eq 'Mg' }))
foreach ($parentMgId in $htManagementGroupsMgPath.($ManagementGroupId).ParentNameChain) {
if ($parentMgId -eq $defaultManagementGroupId) {
$classdefaultMG = 'defaultMG'
}
else {
$classdefaultMG = ''
}
$mgPolicyAssignmentCount = ($totalPolicyAssignmentsMg.where({ $_.AssignmentScopeId -eq $parentMgId })).Count
$mgPolicyPolicySetScopedCount = ($policiesMgScoped.where({ $_.ScopeId -eq $parentMgId }).Count) + ($policySetsMgScoped.where({ $_.ScopeId -eq $parentMgId }).Count)
$mgIdRoleAssignmentCount = $roleAssignmentsMg.where({ $_.AssignmentScopeId -eq $parentMgId }).Count
$html += @"
<ul>
<li><a class="mgnonradius parentmgnotaccessible $($classdefaultMG)">
<div class="main">
<div class="extraInfo">
<div class="extraInfoContent">
"@
if ($mgPolicyAssignmentCount -gt 0 -or $mgPolicyPolicySetScopedCount -gt 0) {
if ($mgPolicyAssignmentCount -gt 0 -and $mgPolicyPolicySetScopedCount -gt 0) {
$html += @"
<div class="extraInfoPolicyAss1">
<abbr class="abbrTree" title="$($mgPolicyAssignmentCount) Policy assignments">$($mgPolicyAssignmentCount)</abbr>
</div>
<div class="extraInfoPolicyScoped1">
<abbr class="abbrTree" title="$($mgPolicyPolicySetScopedCount) Policy/PolicySet definitions scoped">$($mgPolicyPolicySetScopedCount)</abbr>
</div>
"@
}
else {
if ($mgPolicyAssignmentCount -gt 0) {
$html += @"
<div class="extraInfoPolicyAss0">
<abbr class="abbrTree" title="$($mgPolicyAssignmentCount) Policy assignments">$($mgPolicyAssignmentCount)</abbr>
</div>
"@
}
if ($mgPolicyPolicySetScopedCount -gt 0) {
$html += @"
<div class="extraInfoPolicyScoped0">
<abbr class="abbrTree" title="$($mgPolicyPolicySetScopedCount) Policy/PolicySet definitions scoped">$($mgPolicyPolicySetScopedCount)</abbr>
</div>
"@
}
}
}
else {
$html += @'
<div class="extraInfoPlchldr"></div>
'@
}
$html += @'
</div>
<div class="treeMgLogo">
<img class="imgTreeLogo" src="https://www.azadvertizer.net/azgovvizv4/icon/Icon-general-11-Management-Groups.svg">
</div>
<div class="extraInfoContent">
'@
if ($mgIdRoleAssignmentCount -gt 0) {
$html += @"
<div class="extraInfoRoleAss">
<abbr class="abbrTree" title="$($mgIdRoleAssignmentCount) Role assignments">$($mgIdRoleAssignmentCount)</abbr>
</div>
"@
}
else {
$html += @'
<div class="extraInfoPlchldr"></div>
'@
}
$html += @"
</div>
</div>
<div class="fitme" id="fitme">$($parentMgId)
</div>
</div>
</a>
"@
}
$html += @'
<ul>
'@
}
$starthierarchyMap = Get-Date
Write-Host ' Building HierarchyMap'
HierarchyMgHTML -mgChild $ManagementGroupId
showMemoryUsage
$endhierarchyMap = Get-Date
Write-Host " Building HierarchyMap duration: $((New-TimeSpan -Start $starthierarchyMap -End $endhierarchyMap).TotalMinutes) minutes ($((New-TimeSpan -Start $starthierarchyMap -End $endhierarchyMap).TotalSeconds) seconds)"
if ($getMgParentName -eq 'Tenant Root') {
$html += @'
</ul>
</li>
</ul>
</div>
</div>
'@
}
else {
$html += @'
</ul>
</li>
</ul>
</li>
</div>
</ul>
</div>
</div>
'@
}
if (-not $HierarchyMapOnly) {
$html += @'
<div class="summprnt" id="summprnt">
<div class="summary" id="summary"><p class="pbordered">TenantSummary</p>
'@
$html | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force
$html = $null
$startSummary = Get-Date
if (-not $azAPICallConf['htParameters'].NoNetwork) {
if (-not $ManagementGroupsOnly) {
processNetwork
processPrivateEndpoints
}
}
processTenantSummary
showMemoryUsage
#region BuildDailySummaryCSV
if (-not $NoCsvExport) {
$dailySummary4ExportToCSV = [System.Collections.ArrayList]@()
foreach ($entry in $htDailySummary.keys | Sort-Object) {
$null = $dailySummary4ExportToCSV.Add([PSCustomObject]@{
capability = $entry
count = $htDailySummary.($entry)
})
}
Write-Host " Exporting DailySummary CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_DailySummary.csv'"
$dailySummary4ExportToCSV | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_DailySummary.csv" -Delimiter "$csvDelimiter" -NoTypeInformation
}
#endregion BuildDailySummaryCSV
$endSummary = Get-Date
Write-Host " Building TenantSummary duration: $((New-TimeSpan -Start $startSummary -End $endSummary).TotalMinutes) minutes ($((New-TimeSpan -Start $startSummary -End $endSummary).TotalSeconds) seconds)"
$html += @'
</div><!--summary-->
</div><!--summprnt-->
<div class="definitioninsightsprnt" id="definitioninsightsprnt">
<div class="definitioninsights" id="definitioninsights"><p class="pbordered">DefinitionInsights</p>
'@
$html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force
$html = $null
processDefinitionInsights
showMemoryUsage
$html += @'
</div><!--definitionInsights-->
</div><!--definitionInsightsprnt-->
'@
if ((-not $NoScopeInsights) -or (-not $NoSingleSubscriptionOutput)) {
if ((-not $NoScopeInsights)) {
$html += @'
<div class="hierprnt" id="hierprnt">
<div class="hierarchyTables" id="hierarchyTables"><p class="pbordered">ScopeInsights</p>
'@
$html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force
$html = $null
Write-Host ' Building ScopeInsights'
}
$startHierarchyTable = Get-Date
$script:scopescnter = 0
if ($azAPICallConf['htParameters'].NoResources -eq $false) {
if ($azAPICallConf['htParameters'].DoPSRule -eq $true) {
$grpPSRuleSubscriptions = $arrayPsRule | Group-Object -Property subscriptionId
$grpPSRuleManagementGroups = $arrayPsRule | Group-Object -Property mgPath
}
}
if ($arrayFeaturesAll.Count -gt 0) {
$script:subFeaturesGroupedBySubscription = $arrayFeaturesAll | Group-Object -Property subscriptionId
}
if ($arrayOrphanedResourcesSlim.Count -gt 0) {
$arrayOrphanedResourcesGroupedBySubscription = $arrayOrphanedResourcesSlim | Group-Object subscriptionId
}
$resourcesIdsAllCAFNamingRelevantGroupedBySubscription = $resourcesIdsAllCAFNamingRelevant | Group-Object -Property subscriptionId
processScopeInsights -mgChild $ManagementGroupId -mgChildOf $getMgParentId
showMemoryUsage
$endHierarchyTable = Get-Date
Write-Host " Building ScopeInsights duration: $((New-TimeSpan -Start $startHierarchyTable -End $endHierarchyTable).TotalMinutes) minutes ($((New-TimeSpan -Start $startHierarchyTable -End $endHierarchyTable).TotalSeconds) seconds)"
if ((-not $NoScopeInsights)) {
$html += @'
</div>
</div>
'@
}
}
}
$html += @'
<div class="footer">
<div class="VersionDiv VersionLatest"></div>
<div class="VersionDiv VersionThis"></div>
<div class="VersionAlert"></div>
'@
if (-not $HierarchyMapOnly) {
$endAzGovVizHTML = Get-Date
$AzGovVizHTMLDuration = (New-TimeSpan -Start $startAzGovViz -End $endAzGovVizHTML).TotalMinutes
$paramsUsed += "Creation duration: $AzGovVizHTMLDuration minutes "
if (-not $NoScopeInsights) {
$html += @"
<abbr style="text-decoration:none" title="$($paramsUsed)"><i class="fa fa-question-circle" aria-hidden="true"></i></abbr> <button id="hierarchyTreeShowHide" onclick="toggleHierarchyTree()">Hide HierarchyMap</button> <button id="summaryShowHide" onclick="togglesummprnt()">Hide TenantSummary</button> <button id="definitionInsightsShowHide" onclick="toggledefinitioninsightsprnt()">Hide DefinitionInsights</button> <button id="hierprntShowHide" onclick="togglehierprnt()">Hide ScopeInsights</button> $azGovVizNewerVersionAvailableHTML
<hr>
"@
}
else {
$html += @"
<abbr style="text-decoration:none" title="$($paramsUsed)"><i class="fa fa-question-circle" aria-hidden="true"></i></abbr> <button id="hierarchyTreeShowHide" onclick="toggleHierarchyTree()">Hide HierarchyMap</button> <button id="summaryShowHide" onclick="togglesummprnt()">Hide TenantSummary</button> <button id="definitionInsightsShowHide" onclick="toggledefinitioninsightsprnt()">Hide DefinitionInsights</button> $azGovVizNewerVersionAvailableHTML
"@
}
}
$html += @"
</div>
<script src="https://www.azadvertizer.net/azgovvizv4/js/toggle_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/collapsetable_v004_001.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/fitty_v004_001.min.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/version_v004_004.js"></script>
<script src="https://www.azadvertizer.net/azgovvizv4/js/autocorrectOff_v004_001.js"></script>
<script>
fitty('#fitme', {
minSize: 7,
maxSize: 10
});
</script>
<script>
`$("#getImage").on('click', function () {
element = document.getElementById('saveAsImageArea')
var images = element.getElementsByTagName('img');
var l = images.length;
for (var i = 0; i < l; i++) {
images[0].parentNode.removeChild(images[0]);
}
var scale = 3;
domtoimage.toPng(element, { quality: 0.95 , width: element.clientWidth * scale,
height: element.clientHeight * scale,
style: {
transform: 'scale('+scale+')',
transformOrigin: 'top left'
}})
.then(function (dataUrl) {
var link = document.createElement('a');
link.download = '$($fileName).png';
link.href = dataUrl;
link.click();
});
// domtoimage.toJpeg(element)
// .then(function (dataUrl) {
// var link = document.createElement('a');
// link.download = '$($fileName).jpeg';
// link.href = dataUrl;
// link.click();
// });
})
</script>
</body>
</html>
"@
$html | Add-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName).html" -Encoding utf8 -Force
$endBuildHTML = Get-Date
Write-Host "Building HTML total duration: $((New-TimeSpan -Start $startBuildHTML -End $endBuildHTML).TotalMinutes) minutes ($((New-TimeSpan -Start $startBuildHTML -End $endBuildHTML).TotalSeconds) seconds)"
#endregion BuildHTML
buildMD
showMemoryUsage
if (-not $azAPICallConf['htParameters'].NoJsonExport) {
buildJSON
showMemoryUsage
}
if (-not $HierarchyMapOnly) {
buildPolicyAllJSON
}
#endregion createoutputs
apiCallTracking -stage 'Summary' -spacing ''
$endAzGovViz = Get-Date
$durationProduct = (New-TimeSpan -Start $startAzGovViz -End $endAzGovViz)
Write-Host "Azure Governance Visualizer ($ProductVersion) duration: $($durationProduct.TotalMinutes) minutes"
#end
$endTime = Get-Date -Format 'dd-MMM-yyyy HH:mm:ss'
Write-Host "End Azure Governance Visualizer ($ProductVersion) $endTime"
Write-Host 'Checking for errors'
if ($Error.Count -gt 0) {
Write-Host "Dumping $($Error.Count) Errors (handled by Azure Governance Visualizer ($ProductVersion)):"
$Error | Out-Host
}
else {
Write-Host 'Error count is 0'
}
stats
if ($DoTranscript) {
Stop-Transcript
}
Write-Host ''
Write-Host '--------------------'
Write-Host "Azure Governance Visualizer ($ProductVersion) completed successful" -ForegroundColor Green
if ($Error.Count -gt 0) {
Write-Host "Don't bother about dumped errors, execution was successful - if you see errors above this line, those were handled by the tool and are only dumped informational."
}
if ($DoPSRule) {
$psRuleErrors = $arrayPsRule.where({ -not [string]::IsNullOrWhiteSpace($_.errorMsg) })
if ($psRuleErrors) {
Write-Host ''
Write-Host "$($psRuleErrors.Count) 'PSRule for Azure' error(s) encountered"
Write-Host 'Please review the error(s) and consider filing an issue at the PSRule.Rules.Azure GitHub repository https://github.com/Azure/PSRule.Rules.Azure - thank you'
$psRuleErrorsGrouped = $psRuleErrors | Group-Object -Property resourceType, errorMsg
foreach ($errorGroupedByResourceTypeAndMessage in $psRuleErrorsGrouped) {
Write-Host "$($errorGroupedByResourceTypeAndMessage.Count) x $($errorGroupedByResourceTypeAndMessage.Name)"
Write-Host 'Resources:'
foreach ($resourceId in $errorGroupedByResourceTypeAndMessage.Group.resourceId) {
Write-Host " -$resourceId"
}
}
}
}
#region infoNewAzGovVizVersionAvailable
if ($azGovVizNewerVersionAvailable) {
if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) {
Write-Host ''
Write-Host "This Azure Governance Visualizer version ($ProductVersion) is not up to date. Get the latest Azure Governance Visualizer version ($azGovVizVersionOnRepositoryFull)!"
Write-Host 'Check the Azure Governance Visualizer history: https://github.com/Azure/Azure-Governance-Visualizer/blob/master/history.md'
}
}
#endregion infoNewAzGovVizVersionAvailable
#region reportErrors
if ($htResourcePropertiesConvertfromJSONFailed.Keys.Count -gt 0) {
Write-Host ''
Write-Host ' * * * Please help * * *' -ForegroundColor DarkGreen
Write-Host 'For the following resource(s) an error occurred converting from JSON (different casing). Please inspect the resource(s) for keys with different casing. Please file an issue at the Azure Governance Visualizer GitHub repository (aka.ms/AzGovViz) and provide the JSON dump for the resource(s) (scrub subscription Id and company identifyable names) - Thank you!'
foreach ($resourceId in $htResourcePropertiesConvertfromJSONFailed.Keys) {
Write-Host " resId: '$resourceId'"
}
Write-Host ' * * * * * *' -ForegroundColor DarkGreen
}
#endregion reportErrors
#region runIdentifier
if ($ShowRunIdentifier) {
Write-Host "Azure Governance Visualizer ($ProductVersion) run identifier: '$($statsIdentifier)'"
}
#endregion runIdentifier