From 6e28d79f20a980e0005a85a5bf9109d96fba2050 Mon Sep 17 00:00:00 2001 From: quoteee <45695032+JulianHayward@users.noreply.github.com> Date: Mon, 6 Mar 2023 21:55:55 +0100 Subject: [PATCH] v6_major_20230306_1 --- .azuredevops/pipelines/AzGovViz.pipeline.yml | 14 +- .azuredevops/pipelines/AzGovViz.variables.yml | 6 +- .devcontainer/devcontainer.json | 2 +- .github/workflows/AzGovViz.yml | 12 +- .github/workflows/AzGovViz_OIDC.yml | 12 +- README.md | 112 ++-- history.md | 26 +- img/insights_map_pwsh.png | Bin 0 -> 77238 bytes img/orphaned_stoppedVMs.png | Bin 0 -> 22151 bytes pwsh/AzGovVizParallel.ps1 | 618 ++++++++++++------ pwsh/dev/devAzGovVizParallel.ps1 | 90 ++- pwsh/dev/functions/addHtParameters.ps1 | 4 +- pwsh/dev/functions/buildMD.ps1 | 6 +- pwsh/dev/functions/buildPolicyAllJSON.ps1 | 84 +-- pwsh/dev/functions/cacheBuiltIn.ps1 | 91 +-- pwsh/dev/functions/checkAzGovVizVersion.ps1 | 2 +- .../dataCollectionFunctions.ps1 | 39 +- pwsh/dev/functions/detailSubscriptions.ps1 | 2 +- pwsh/dev/functions/detectPolicyEffect.ps1 | 84 +++ pwsh/dev/functions/getConsumption.ps1 | 3 +- pwsh/dev/functions/getOrphanedResources.ps1 | 12 +- pwsh/dev/functions/getPolicyHash.ps1 | 9 + pwsh/dev/functions/getSubscriptions.ps1 | 10 +- pwsh/dev/functions/processDataCollection.ps1 | 12 +- .../functions/processScopeInsightsMgOrSub.ps1 | 8 +- .../processStorageAccountAnalysis.ps1 | 2 +- pwsh/dev/functions/processTenantSummary.ps1 | 147 ++++- pwsh/dev/functions/runInfo.ps1 | 8 +- pwsh/dev/functions/validateAccess.ps1 | 8 +- setup.md | 205 +++--- version.txt | 2 +- 31 files changed, 1034 insertions(+), 596 deletions(-) create mode 100644 img/insights_map_pwsh.png create mode 100644 img/orphaned_stoppedVMs.png create mode 100644 pwsh/dev/functions/detectPolicyEffect.ps1 create mode 100644 pwsh/dev/functions/getPolicyHash.ps1 diff --git a/.azuredevops/pipelines/AzGovViz.pipeline.yml b/.azuredevops/pipelines/AzGovViz.pipeline.yml index 77fc638d..86a2df61 100644 --- a/.azuredevops/pipelines/AzGovViz.pipeline.yml +++ b/.azuredevops/pipelines/AzGovViz.pipeline.yml @@ -1,4 +1,4 @@ -# AzGovViz v6_major_20221212_1 +# Azure Governance Visualizer v6_major_20230306_1 # First things first: # 1. Mandatory: In the AzGovViz.variables.yml file set needed variables 'ServiceConnection' and 'ManagementGroupId # 2. Mandatory: Check line 20 @@ -19,7 +19,7 @@ schedules: include: - master #CHECK branch 'master' is applicable? - delete me :) -#Running AzOps? Run AzGovViz after 'AzOps - Push' .. +#Running AzOps? Run Azure Governance Visualizer after 'AzOps - Push' .. #AzOps Accellerator https://github.com/Azure/AzOps-Accelerator #resources: # pipelines: @@ -31,7 +31,7 @@ schedules: # - master #CHECK branch 'master' is applicable? - delete me :) jobs: -- job: AzGovViz +- job: AzureGovernanceVisualizer timeoutInMinutes: 0 pool: @@ -113,22 +113,22 @@ jobs: scriptPath: '$(System.DefaultWorkingDirectory)/$(ScriptDir)/$(Script)' scriptArguments: $(ScriptArguments) -ScriptPath $(ScriptDir) azurePowerShellVersion: latestVersion - displayName: 'Run AzGovViz' + displayName: 'Run Azure Governance Visualizer' - pwsh: | write-host "#################################" - write-host "Push AzGovViz output to repository" + write-host "Push Azure Governance Visualizer output to repository" write-host "#################################" $executionDateTimeInternationalReadable = get-date -format "dd-MMM-yyyy HH:mm:ss" $currentTimeZone = (Get-TimeZone).Id - git config --global user.email "AzGovVizPipeline@azdo.com" + git config --global user.email "AzureGovernanceVisualizerPipeline@azdo.com" $PipelineInfo = "Pipeline: '$(Build.DefinitionName)' 'rev $(Build.BuildNumber)' (Project: $([uri]::EscapeDataString("$(System.TeamProject)")); Repository: $(Build.Repository.Name); Branch: $(Build.SourceBranchName) Commit: $(Build.SourceVersion))" git config --global user.name "$PipelineInfo" git config pull.rebase false git add --all git commit -m "wiki $executionDateTimeInternationalReadable ($currentTimeZone)" git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" push origin HEAD:$(Build.SourceBranchName) - displayName: 'Push AzGovViz output to repository' + displayName: 'Push Azure Governance Visualizer output to repository' - task: AzurePowerShell@5 condition: and(succeeded(), eq(variables['WebAppPublish'], 'true')) diff --git a/.azuredevops/pipelines/AzGovViz.variables.yml b/.azuredevops/pipelines/AzGovViz.variables.yml index baa1a155..3a8d9bec 100644 --- a/.azuredevops/pipelines/AzGovViz.variables.yml +++ b/.azuredevops/pipelines/AzGovViz.variables.yml @@ -1,4 +1,4 @@ -# AzGovViz v6_major_20221213_1 +# Azure Governance Visualizer v6_major_20230306_1 # First things first: # 1. Replace with the name of your service connection # 2. Replace with the your ManagementGroupId @@ -64,7 +64,7 @@ variables: ### Default Variables - Modify as Needed - # If you integrate AzGovViz into an existing repository you may need to adjust the script directory + # If you integrate Azure Governance Visualizer into an existing repository you may need to adjust the script directory - name: ScriptDir #example: YourFolder/pwsh value: pwsh @@ -85,7 +85,7 @@ variables: # String | default = ';' | example: value: ';' value: ';' - # Specifies the path to output the results from AzGovViz + # Specifies the path to output the results from Azure Governance Visualizer - name: OutputPath # String | example: value: 'wiki' value: 'wiki' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 20f17e71..72c90fc1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "AzGovViz", + "name": "AzureGovernanceVisualizer", "dockerFile": "Dockerfile", "settings": { "terminal.integrated.defaultProfile.linux": "pwsh" diff --git a/.github/workflows/AzGovViz.yml b/.github/workflows/AzGovViz.yml index f900ced4..0c43f70e 100644 --- a/.github/workflows/AzGovViz.yml +++ b/.github/workflows/AzGovViz.yml @@ -1,10 +1,10 @@ -# AzGovViz v6_major_20220930_1 +# Azure Governance Visualizer v6_major_20230306_1 # First things first: # 1. Mandatory: define in line 11 # 2. Optional: enable the schedule (line 21,22) # Documentation: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting -name: AzGovViz +name: AzureGovernanceVisualizer env: OutputPath: wiki @@ -25,7 +25,7 @@ on: workflow_dispatch: jobs: - AzGovViz: + Azure Governance Visualizer: runs-on: ubuntu-latest steps: @@ -54,16 +54,16 @@ jobs: . .\$($env:ScriptDir)\$($env:ScriptPrereqFile) -OutputPath ${env:OutputPath} azPSVersion: "latest" - - name: Run AzGovViz + - name: Run Azure Governance Visualizer uses: azure/powershell@v1 with: inlineScript: | . .\$($env:ScriptDir)\$($env:ScriptFile) -ManagementGroupId ${env:ManagementGroupId} -ScriptPath ${env:ScriptDir} -OutputPath ${env:OutputPath} azPSVersion: "latest" - - name: Push AzGovViz output to repository + - name: Push Azure Governance Visualizer output to repository run: | - git config --global user.email "AzGovVizGHActions@ghActions.com" + git config --global user.email "AzureGovernanceVisualizerGHActions@ghActions.com" git config --global user.name "$GITHUB_ACTOR" git config pull.rebase false git add --all diff --git a/.github/workflows/AzGovViz_OIDC.yml b/.github/workflows/AzGovViz_OIDC.yml index b8194038..d1528123 100644 --- a/.github/workflows/AzGovViz_OIDC.yml +++ b/.github/workflows/AzGovViz_OIDC.yml @@ -1,10 +1,10 @@ -# AzGovViz v6_major_20220930_1 +# Azure Governance Visualizer v6_major_20230306_1 # First things first: # 1. Mandatory: define in line 11 # 2. Optional: enable the schedule (line 22,23) # Documentation: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting -name: AzGovViz_OIDC +name: AzureGovernanceVisualizer_OIDC env: OutputPath: wiki @@ -31,7 +31,7 @@ permissions: contents: write jobs: - AzGovViz: + AzureGovernanceVisualizer: runs-on: ubuntu-latest steps: @@ -53,16 +53,16 @@ jobs: . .\$($env:ScriptDir)\$($env:ScriptPrereqFile) -OutputPath ${env:OutputPath} azPSVersion: "latest" - - name: Run AzGovViz + - name: Run Azure Governance Visualizer uses: azure/powershell@v1 with: inlineScript: | . .\$($env:ScriptDir)\$($env:ScriptFile) -ManagementGroupId ${env:ManagementGroupId} -SubscriptionId4AzContext ${{secrets.SUBSCRIPTION_ID}} -ScriptPath ${env:ScriptDir} -OutputPath ${env:OutputPath} -GitHubActionsOIDC azPSVersion: "latest" - - name: Push AzGovViz output to repository + - name: Push Azure Governance Visualizer output to repository run: | - git config --global user.email "AzGovVizGHActions@ghActions.com" + git config --global user.email "AzureGovernanceVisualizerGHActions@ghActions.com" git config --global user.name "azgvz" git config pull.rebase false git add --all diff --git a/README.md b/README.md index 863a764a..dd9f03c5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# AzGovViz - Azure Governance Visualizer +# Azure Governance Visualizer aka AzGovViz Do you want to get granular insights on your technical Azure Governance implementation? - document it in CSV, HTML, Markdown and JSON? -AzGovViz is a PowerShell based script that iterates your Azure Tenant´s 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 AzGovViz provides visibility on your __HierarchyMap__, creates a __TenantSummary__, creates __DefinitionInsights__ and builds granular __ScopeInsights__ on Management Groups and Subscriptions. The technical requirements as well as the required permissions are minimal. +Azure Governance Visualizer is a PowerShell based script that iterates your Azure Tenant´s 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 __HierarchyMap__, creates a __TenantSummary__, creates __DefinitionInsights__ and builds granular __ScopeInsights__ on Management Groups and Subscriptions. The technical requirements as well as the required permissions are minimal. You can run the script either for your Tenant Root Group or any other Management Group. @@ -14,11 +14,11 @@ Challenges: * Holistic overview on governance implementation * Connecting the dots -AzGovViz is intended to help you to get a holistic overview on your technical Azure Governance implementation by __connecting the dots__ +Azure Governance Visualizer is intended to help you to get a holistic overview on your technical Azure Governance implementation by __connecting the dots__ ![ConnectingDot](img/AzGovVizConnectingDots_v4.2.png) -## AzGovViz @ Microsoft CAF & WAF +## Azure Governance Visualizer @ Microsoft CAF & WAF ### Microsoft Cloud Adoption Framework (CAF) @@ -34,9 +34,9 @@ Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/arch ![ChatGPT](img/chatGPT.png) ## Content -- [AzGovViz - Azure Governance Visualizer](#azgovviz---azure-governance-visualizer) +- [Azure Governance Visualizer aka AzGovViz](#azure-governance-visualizer-aka-azgovviz) - [Mission](#mission) - - [AzGovViz @ Microsoft CAF \& WAF](#azgovviz--microsoft-caf--waf) + - [Azure Governance Visualizer @ Microsoft CAF \& WAF](#azure-governance-visualizer--microsoft-caf--waf) - [Microsoft Cloud Adoption Framework (CAF)](#microsoft-cloud-adoption-framework-caf) - [Microsoft Well Architected Framework (WAF)](#microsoft-well-architected-framework-waf) - [ChatGPT](#chatgpt) @@ -48,7 +48,8 @@ Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/arch - [Features](#features) - [Screenshots](#screenshots) - [Outputs](#outputs) - - [AzGovViz Setup Guide](#azgovviz-setup-guide) + - [Trust](#trust) + - [Azure Governance Visualizer Setup Guide](#azure-governance-visualizer-setup-guide) - [Technical documentation](#technical-documentation) - [Permissions overview](#permissions-overview) - [Required permissions in Azure](#required-permissions-in-azure) @@ -70,14 +71,26 @@ Listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/arch ## Release history -__Changes__ (2023-Mar-02 / Major) - -* Extended the Orphaned Resource with the capability to see "Stopped" virtual machines. These stopped virtual machines are still generated costs. You better should set these virtual machines to "Stopped (deallocated)" to save costs. - -Passed tests: Powershell Core 7.3.1 on Windows -Passed tests: Powershell Core 7.2.7 Azure DevOps hosted agent ubuntu-22.04 -Passed tests: Powershell Core 7.2.7 Github Actions hosted agent ubuntu-latest -Passed tests: Powershell Core 7.2.6 GitHub Codespaces mcr.microsoft.com/powershell:latest +__Changes__ (2023-Mar-06 / Major) + +* New feature: Custom Policy definitions that have 'Policy rule' parity with built-in Policy definition(s) (HTML __TenantSummary__/Policy and CSV output) +* Enhanced *_PolicyAll.json output to include Policy assignments +* Optimize method to detect Policy definition effect +* Optimize consumption / convert to decimal +* Renamed the 'Orphaned Resources' feature to 'Cost optimization & cleanup' + * Renamed CSV output '\_\*ResourcesOrphaned.csv' to '\_\*ResourcesCostOptimizationAndCleanup.csv' +* 🚀 Highlight contribution by @TimWanierke: Extended the 'Cost optimization & cleanup' feature (HTML __TenantSummary__/Subscriptions, Resources & Defender) with 'stopped but not deallocated' Virtual Machines including the related cost +![stoppedVMs](img/orphaned_stoppedVMs.png) +* Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.70 + * minor fixes; not Azure Governance Visualizer specific +* Added new section [Trust?!](#trust) +* Typ0s and minor fixes +* Transitioning product name 'AzGovViz' to 'Azure Governance Visualizer aka AzGovViz' as folks tend to interpretate the 'Gov' as government and not as governance + +Passed tests: Powershell Core 7.3.3 on Windows +Passed tests: Powershell Core 7.2.10 Azure DevOps hosted agent ubuntu-22.04 +Passed tests: Powershell Core 7.2.10 Github Actions hosted agent ubuntu-latest +Passed tests: Powershell Core 7.2.10 GitHub Codespaces mcr.microsoft.com/powershell:latest Passed tests: AzureCloud, AzureUSGovernment, AzureChinaCloud [Full release history](history.md) @@ -93,12 +106,12 @@ More [demo output](https://github.com/JulianHayward/AzGovViz) ### Media * Microsoft Tech Talks - Bevan Sinclair (Cloud Solution Architect Microsoft) [Automated Governance Reporting in Azure (MTT0AEDT)](https://mtt.eventbuilder.com/event/66431) (register to view) -* Microsoft Dev Radio (YouTube) [Get visibility into your environment with AzGovViz](https://www.youtube.com/watch?v=hZXvF5oypLE) -* Jack Tracey (Cloud Solution Architect Microsoft) [AzGovViz With Azure DevOps](https://jacktracey.co.uk/azgovviz-with-azure-devops/) +* Microsoft Dev Radio (YouTube) [Get visibility into your environment with Azure Governance Visualizer](https://www.youtube.com/watch?v=hZXvF5oypLE) +* Jack Tracey (Cloud Solution Architect Microsoft) [Azure Governance Visualizer With Azure DevOps](https://jacktracey.co.uk/azgovviz-with-azure-devops/) ### Slideset -Short presentation on AzGovViz [[download](slides/AzGovViz_intro.pdf)] +Short presentation on Azure Governance Visualizer [[download](slides/AzGovViz_intro.pdf)] ## Features @@ -142,7 +155,7 @@ Short presentation on AzGovViz [[download](slides/AzGovViz_intro.pdf)] * Resolved Managed Identity (if Policy effect is DeployIfNotExists (DINE) or Modify) * System metadata 'createdOn, createdBy, updatedOn, updatedBy' ('createdBy', 'updatedBy' identity is fully resolved) * Parameters used - * ALZ EverGreen - Azure Landing Zones EverGreen for Policy and Set definitions. AzGovViz 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 ALZ EverGreen results will be displayed in the __TenantSummary__ and a CSV export `*_ALZEverGreen.csv` will be provided. + * ALZ EverGreen - Azure Landing Zones EverGreen 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 ALZ EverGreen results will be displayed in the __TenantSummary__ and a CSV export `*_ALZEverGreen.csv` will be provided. * __Role-Based Access Control (RBAC)__ * Custom Role definitions * List assignable scopes @@ -159,7 +172,7 @@ Short presentation on AzGovViz [[download](slides/AzGovViz_intro.pdf)] * Core information on Role assignments * Advanced information on Role assignments * Role assignment scope (at scope / inheritance) - * For Role Assignments on Groups the AAD Group members are fully resolved. With this capability AzGovViz can ultimately provide holistic insights on permissions granted + * For Role Assignments on Groups the AAD Group members are fully resolved. With this capability Azure Governance Visualizer can ultimately provide holistic insights on permissions granted * For Role Assignments on Groups the AAD Group members count (transitive) will be reported * For identity-type == 'ServicePrincipal' the type (Application (internal/external) / ManagedIdentity (System assigned/User assigned)) will be revealed * For identity-type == 'User' the userType (Member/Guest) will be revealed @@ -207,12 +220,12 @@ Short presentation on AzGovViz [[download](slides/AzGovViz_intro.pdf)] * Resource Locks * Aggregated insights for Lock and respective Lock-type usage on Subscriptions, ResourceGroups and Resources * CSV output detailed / each scope that has a lock applied (at scope) - * Resource fluctuation - added/removed resources since previous AzGovViz execution + * Resource fluctuation - added/removed resources since previous Azure Governance Visualizer execution * Aggregated insights on resource fluctuation add/remove (HTML) * Detailed insights on resource fluctuation add/remove (CSV) - * Orphaned Resources (ARG) - * If you run AzGovViz with parameter -DoAzureConsumption then the orphaned resources output will show you potential cost savings for orphaned resources with intent 'cost savings' - * The orphaned resources feature is based on [Azure Orphan Resources - GitHub](https://github.com/dolevshor/azure-orphan-resources) ARG queries and workbooks by Dolev Shor + * Cost optimization & cleanup (ARG) + * If you run Azure Governance Visualizer with parameter -DoAzureConsumption then the orphaned/unused resources output will show you potential cost savings for orphaned/unused resources with intent 'cost savings' + * The Cost optimization & cleanup feature is based on [Azure Orphan Resources - GitHub](https://github.com/dolevshor/azure-orphan-resources) ARG queries and workbooks by Dolev Shor * The virtual machines that are stopped but not deallocated are still generating compute costs. You should check if there are virtual machines running within your environment that are only stopped. The output will show these virtual machines with intent 'cost savings - stopped but not deallocated VM'. * Cloud Adoption Framework (CAF) [Recommended abbreviations for Azure resource types](https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations) compliance * Microsoft Defender for Cloud @@ -223,7 +236,7 @@ Short presentation on AzGovViz [[download](slides/AzGovViz_intro.pdf)] * Summary of all UserAssigned Managed Identities assigned to Resources * Summary of Resources that have an UserAssigned Managed Identity assigned * [Integrate PSRule for Azure](#integrate-psrule-for-azure) - * __Pausing 'PSRule for Azure' integration__. AzGovViz 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. + * __Pausing 'PSRule for Azure' integration__. 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. * Well-Architected Framework aligned best practice analysis for resources, including guidance for remediation * Storage Account Access Analysis * Provides insights on Storage Accounts with focus on anonymous access (containers/blobs and 'Static website' feature) @@ -318,7 +331,24 @@ markdown in Azure DevOps Wiki as Code * Tenant tree including all Policy and Role assignments AND all Custom Policy/Set and Role definitions ![alt text](img/jsonfolderfull450.jpg "JSONFolder") -## AzGovViz Setup Guide +## Trust + +_How can we trust a 20k lines PowerShell script?_ Besides assuring that Azure Governance Visualizer will not harm at any intent, you may want to secure yourself. Let´s leverage Azure built-in capabilities such as VM Insights to monitor the Azure Governance Visualizer activity. + +Setup a Virtual Machine in Azure, deploy the dependency agent extension and [execute](setup.md#azure-governance-visualizer-from-console) Azure Governance Visualizer. + +In the Azure Portal navigate to the Virtual Machine, Click on __Insights__ in the __Monitoring__ section and click on Map. All connections that have been established will be shown. Now let´s focus on the process __pwsh__ and review the established connections. + +![alt text](img/insights_map_pwsh.png "JSONFolder") + +Query for Log Analytics: +``` +VMConnection +| where AgentId =~ '' +| where ProcessName =~ 'pwsh' +| summarize by DestinationIp, DestinationPort, RemoteIp, Protocol, Direction, RemoteDnsQuestions, BytesSent, BytesReceived +``` +## Azure Governance Visualizer Setup Guide 💡 Although 30 minutes of troubleshooting can save you 5 minutes reading the documentation :) .. Check the detailed __[Setup Guide](setup.md)__ @@ -494,11 +524,11 @@ AzAPICall resources: * `-StatsOptOut` - Opt out sending [stats](#stats) * `-NoSingleSubscriptionOutput` - Single __Scope Insights__ output per Subscription should not be created * `-ManagementGroupsOnly` - Collect data only for Management Groups (Subscription data such as e.g. Policy assignments etc. will not be collected) - * `-ShowMemoryUsage` - Shows memory usage at memory intense sections of the scripts, this shall help you determine if the the worker is well sized for AzGovViz + * `-ShowMemoryUsage` - Shows memory usage at memory intense sections of the scripts, this shall help you determine if the the worker is well sized for Azure Governance Visualizer * `-CriticalMemoryUsage` - Define at what percentage of memory usage the garbage collection should kick in (default=90) * `-ExcludedResourceTypesDiagnosticsCapable` - Resource Types to be excluded from processing analysis for diagnostic settings capability (default: microsoft.web/certificates) * PSRule for Azure - * __Pausing 'PSRule for Azure' integration__. AzGovViz 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. + * __Pausing 'PSRule for Azure' integration__. 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` - Execute [PSRule for Azure](https://azure.github.io/PSRule.Rules.Azure). Aggregated results are integrated in the HTML output, detailed results (per resource) are exported to CSV * `-PSRuleVersion` - Define the PSRule..Rules.Azure PowerShell module version, if undefined then 'latest' will be used * `-PSRuleFailedOnly` - PSRule for Azure will only report on failed resource (may save some space/noise). (e.g. `.\pwsh\AzGovVizParallel.ps1 -DoPSRule -PSRuleFailedOnly`) @@ -516,7 +546,7 @@ AzAPICall resources: ### API reference -AzGovViz polls the following APIs +Azure Governance Visualizer polls the following APIs | Endpoint | API version | API name | | -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | @@ -582,7 +612,7 @@ AzGovViz polls the following APIs ## Integrate with AzOps Did you know you can run AzOps from Azure DevOps? Check [AzOps Accellerator](https://github.com/Azure/AzOps-Accelerator). -You can integrate AzGovViz (same project as AzOps). +You can integrate Azure Governance Visualizer (same project as AzOps). ```yaml pipelines: @@ -596,7 +626,7 @@ You can integrate AzGovViz (same project as AzOps). ## Integrate PSRule for Azure -__Pausing 'PSRule for Azure' integration__. AzGovViz 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. +__Pausing 'PSRule for Azure' integration__. 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. Let´s use [PSRule for Azure](https://azure.github.io/PSRule.Rules.Azure) and leverage over 260 pre-built rules to validate Azure resources based on the Microsoft Well-Architected Framework (WAF) principles. PSRule for Azure is listed as [security monitoring tool](https://docs.microsoft.com/en-us/azure/architecture/framework/security/monitor-tools) in the Microsoft Well-Architected Framework. @@ -615,11 +645,11 @@ Outputs: * CSV (detailed, per resource) TenantSummary HTML output example: -![alt text](img/PSRuleForAzure_preview.png "PSRule for Azure / AzGovViz TenantSummary") +![alt text](img/PSRuleForAzure_preview.png "PSRule for Azure / Azure Governance Visualizer TenantSummary") ## Stats -In order to better understand the AzGovViz usage and to optimize the product accordingly some stats will be ingested to Azure Application Insights. Results of stats analysis may be shared at a later stage. +In order to better understand the Azure Governance Visualizer usage and to optimize the product accordingly some stats will be ingested to Azure Application Insights. Results of stats analysis may be shared at a later stage. ### How/What? @@ -649,7 +679,7 @@ The following data will be ingested to Azure Application Insights: "azCloud": "Azure environment e.g. AzureCloud, ChinaCloud, etc.", "identifier": "8c62a7..53d08c0 (string of 128 characters)", "platform": "Console / AzureDevOps", - "productVersion": "used AzGovViz version", + "productVersion": "used Azure Governance Visualizer version", "psAzAccountsVersion": "used Az.Accounts PS module version", "psVersion": "used PowerShell version", "scopeUsage": "childManagementGroup / rootManagementGroup", @@ -675,7 +705,7 @@ Azure Application Insights data: ![alt text](img/stats.jpg "Stats") -If you do not want to contribute to stats for AzGovViz then you can use the parameter: +If you do not want to contribute to stats for Azure Governance Visualizer then you can use the parameter: `-StatsOptOut` If you have any concerns or see a risk sending stats please file an issue. @@ -684,9 +714,9 @@ Thank you for your support! ## Security -AzGovViz creates very detailed information about your Azure Governance setup. In your organization's best interest the __outputs should be protected from not authorized access!__ +Azure Governance Visualizer creates very detailed information about your Azure Governance setup. In your organization's best interest the __outputs should be protected from not authorized access!__ -Azure Defender for Cloud may alert AzGovViz resource queries as suspicious activity: +Azure Defender for Cloud may alert Azure Governance Visualizer resource queries as suspicious activity: ![alt text](img/azgvz_MDfC_securityAlert.png "Microsoft defender for Cloud security alert") ## Known issues @@ -716,11 +746,11 @@ You are welcome to contribute to the project. __[Contribution Guide](contributio Thanks to so many supporters - testing, giving feedback, making suggestions, presenting use-case, posting/blogging articles, refactoring code - THANK YOU! -Thanks Stefan Stranger (Microsoft) for providing me with his AzGovViz outputs executed on his implementation of EnterpriseScale. Make sure you read Stefan´s Blog Article: [Enterprise-Scale - Policy Driven Governance](https://stefanstranger.github.io/2020/08/28/EnterpriseScalePolicyDrivenGovernance) +Thanks Stefan Stranger (Microsoft) for providing me with his Azure Governance Visualizer outputs executed on his implementation of EnterpriseScale. Make sure you read Stefan´s Blog Article: [Enterprise-Scale - Policy Driven Governance](https://stefanstranger.github.io/2020/08/28/EnterpriseScalePolicyDrivenGovernance) -Thanks Frank Oltmanns-Mack (Microsoft) for providing me with his AzGovViz outputs executed on his implementation of EnterpriseScale. +Thanks Frank Oltmanns-Mack (Microsoft) for providing me with his Azure Governance Visualizer outputs executed on his implementation of EnterpriseScale. -Carlos Mendible (Microsoft) gracias por tu contribución on the project - run AzGovViz with GitHub Codespaces. +Carlos Mendible (Microsoft) gracias por tu contribución on the project - run Azure Governance Visualizer with GitHub Codespaces. Special thanks to Tim Wanierke, Brooks Vaughn and Friedrich Weinmann (Microsoft). @@ -750,4 +780,4 @@ Also check - What about your Azure ## Closing Note -Please note that while being developed by a Microsoft employee, AzGovViz is not a Microsoft service or product. AzGovViz 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/history.md b/history.md index 81a58fd9..b522f650 100644 --- a/history.md +++ b/history.md @@ -1,8 +1,24 @@ -# AzGovViz - Azure Governance Visualizer - -## AzGovViz version history - -### AzGovViz version 6 +# Azure Governance Visualizer aka AzGovViz + +## Azure Governance Visualizer version history + +### Azure Governance Visualizer version 6 + +__Changes__ (2023-Mar-06 / Major) + +* New feature: Custom Policy definitions that have 'Policy rule' parity with built-in Policy definition(s) (HTML __TenantSummary__/Policy and CSV output) +* Enhanced *_PolicyAll.json output to include Policy assignments +* Optimize method to detect Policy definition effect +* Optimize consumption / convert to decimal +* Renamed the 'Orphaned Resources' feature to 'Cost optimization & cleanup' + * Renamed CSV output '\_\*ResourcesOrphaned.csv' to '\_\*ResourcesCostOptimizationAndCleanup.csv' +* 🚀 Highlight contribution by @TimWanierke: Extended the 'Cost optimization & cleanup' feature (HTML __TenantSummary__/Subscriptions, Resources & Defender) with 'stopped but not deallocated' Virtual Machines including the related cost +![stoppedVMs](img/orphaned_stoppedVMs.png) +* Use [AzAPICall](https://aka.ms/AzAPICall) PowerShell module version 1.1.70 + * minor fixes; not Azure Governance Visualizer specific +* Added new section [Trust?!](#trust) +* Typ0s and minor fixes +* Transitioning product name 'AzGovViz' to 'Azure Governance Visualizer aka AzGovViz' as folks tend to interpretate the 'Gov' as government and not as governance __Changes__ (2023-Mar-02 / Major) diff --git a/img/insights_map_pwsh.png b/img/insights_map_pwsh.png new file mode 100644 index 0000000000000000000000000000000000000000..e525f0bddcc7f2423feddac27a72835bcdccf547 GIT binary patch literal 77238 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

YiZo- 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 literal 0 HcmV?d00001 diff --git a/pwsh/AzGovVizParallel.ps1 b/pwsh/AzGovVizParallel.ps1 index 3a8fa179..603a4a59 100644 --- a/pwsh/AzGovVizParallel.ps1 +++ b/pwsh/AzGovVizParallel.ps1 @@ -10,7 +10,7 @@ Management Groups, Subscriptions .DESCRIPTION - Do you want to get granular insights on your technical Azure Governance implementation? - document it in csv, html and markdown? AzGovViz 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 AzGovViz 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. + 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 @@ -34,7 +34,7 @@ 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 AzGovViz 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_ + 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 NoPolicyComplianceStates use this parameter if policy compliance states should not be queried @@ -157,7 +157,7 @@ PS C:\>.\AzGovVizParallel.ps1 -ManagementGroupId -NoPIMEligibilityIntegrationRoleAssignmentsAll .PARAMETER NoALZPolicyVersionChecker - 'Azure Landing Zones (ALZ) Policy Version Checker' for Policy and Set definitions. AzGovViz 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. + '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 -NoALZPolicyVersionChecker @@ -349,7 +349,7 @@ .LINK https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting (aka.ms/AzGovViz) https://github.com/microsoft/CloudAdoptionFramework/tree/master/govern/AzureGovernanceVisualizer - Please note that while being developed by a Microsoft employee, AzGovViz is not a Microsoft service or product. AzGovViz 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. + 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()] @@ -359,10 +359,10 @@ Param $Product = 'AzGovViz', [string] - $AzAPICallVersion = '1.1.68', + $AzAPICallVersion = '1.1.70', [string] - $ProductVersion = 'v6_major_20230302_1', + $ProductVersion = 'v6_major_20230306_1', [string] $GithubRepository = 'aka.ms/AzGovViz', @@ -604,7 +604,10 @@ Param $LimitTagsSubscription = 50, [array] - $MSTenantIds = @('2f4a9838-26b7-47ee-be60-ccc1fdec5953', '33e01921-4d64-4f8c-a055-5bdaffd5e33d') + $MSTenantIds = @('2f4a9838-26b7-47ee-be60-ccc1fdec5953', '33e01921-4d64-4f8c-a055-5bdaffd5e33d'), + + [array] + $ValidPolicyEffects = @('append', 'audit', 'auditIfNotExists', 'deny', 'deployIfNotExists', 'modify', 'manual', 'disabled', 'EnforceRegoPolicy', 'enforceSetting') ) $Error.clear() @@ -616,7 +619,7 @@ Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings 'true' #start $startAzGovViz = Get-Date $startTime = Get-Date -Format 'dd-MMM-yyyy HH:mm:ss' -Write-Host "Start AzGovViz $($startTime) (#$($ProductVersion))" +Write-Host "Start Azure Governance Visualizer $($startTime) (#$($ProductVersion))" if ($ManagementGroupId -match ' ') { Write-Host "Provided Management Group ID: '$($ManagementGroupId)'" -ForegroundColor Yellow @@ -626,7 +629,7 @@ if ($ManagementGroupId -match ' ') { #region Functions function addHtParameters { - Write-Host 'Add AzGovViz htParameters' + Write-Host 'Add Azure Governance Visualizer htParameters' if ($LargeTenant -eq $true) { $script:NoScopeInsights = $true $NoResourceProvidersAtAll = $true @@ -669,7 +672,7 @@ function addHtParameters { } Write-Host 'htParameters:' $azAPICallConf['htParameters'] | Format-Table -AutoSize | Out-String - Write-Host 'Add AzGovViz htParameters succeeded' -ForegroundColor Green + Write-Host 'Add Azure Governance Visualizer htParameters succeeded' -ForegroundColor Green } function addIndexNumberToArray ( [Parameter(Mandatory = $True)] @@ -1346,7 +1349,7 @@ function buildMD { if ($azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions -eq $true) { if ($azAPICallConf['htParameters'].onAzureDevOps -eq $true) { $markdown += @" -# AzGovViz - Management Group Hierarchy +# Azure Governance Visualizer - Management Group Hierarchy ## HierarchyMap (Mermaid) @@ -1357,7 +1360,7 @@ function buildMD { if ($azAPICallConf['htParameters'].onGitHubActions -eq $true) { $marks = '```' $markdown += @" -# AzGovViz - Management Group Hierarchy +# Azure Governance Visualizer - Management Group Hierarchy ## HierarchyMap (Mermaid) @@ -1369,7 +1372,7 @@ $($marks)mermaid } else { $markdown += @" -# AzGovViz - Management Group Hierarchy +# Azure Governance Visualizer - Management Group Hierarchy $executionDateTimeInternationalReadable ($currentTimeZone) @@ -1505,52 +1508,58 @@ function buildPolicyAllJSON { $htPolicyAndPolicySet = [ordered]@{} $htPolicyAndPolicySet.Policy = [ordered]@{} $htPolicyAndPolicySet.PolicySet = [ordered]@{} + $htPolicyAndPolicySet.PolicyAssignment = [ordered]@{} foreach ($policy in ($tenantPoliciesDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicyDefinitionId)) { - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()) = [ordered]@{} - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyType = $policy.Type - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).ScopeMGLevel = $policy.ScopeMGLevel - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).Scope = $policy.Scope - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).ScopeId = $policy.scopeId - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyDisplayName = $policy.PolicyDisplayName - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyDefinitionName = $policy.PolicyDefinitionName - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyDefinitionId = $policy.PolicyDefinitionId - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyEffect = $policy.PolicyEffect - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).PolicyCategory = $policy.PolicyCategory - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UniqueAssignmentsCount = $policy.UniqueAssignmentsCount - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UniqueAssignments = $policy.UniqueAssignments - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UsedInPolicySetsCount = $policy.UsedInPolicySetsCount - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UsedInPolicySets = $policy.UsedInPolicySet4JSON - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).CreatedOn = $policy.CreatedOn - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).CreatedBy = $policy.CreatedByJson - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UpdatedOn = $policy.UpdatedOn - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).UpdatedBy = $policy.UpdatedByJson - $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()).JSON = $policy.Json + $htPolicyAndPolicySet.Policy.($policy.PolicyDefinitionId.ToLower()) = [ordered]@{ + PolicyType = $policy.Type + ScopeMGLevel = $policy.ScopeMGLevel + Scope = $policy.Scope + ScopeId = $policy.scopeId + PolicyDisplayName = $policy.PolicyDisplayName + PolicyDefinitionName = $policy.PolicyDefinitionName + PolicyDefinitionId = $policy.PolicyDefinitionId + PolicyEffect = $policy.PolicyEffect + PolicyCategory = $policy.PolicyCategory + UniqueAssignmentsCount = $policy.UniqueAssignmentsCount + UniqueAssignments = $policy.UniqueAssignments + UsedInPolicySetsCount = $policy.UsedInPolicySetsCount + UsedInPolicySets = $policy.UsedInPolicySet4JSON + CreatedOn = $policy.CreatedOn + CreatedBy = $policy.CreatedByJson + UpdatedOn = $policy.UpdatedOn + UpdatedBy = $policy.UpdatedByJson + JSON = $policy.Json + } } foreach ($policySet in ($tenantPolicySetsDetailed | Sort-Object -Property Type, ScopeMGLevel, PolicySetDefinitionId)) { - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()) = [ordered]@{} - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PolicySetType = $policy.Type - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).ScopeMGLevel = $policySet.ScopeMGLevel - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).Scope = $policySet.Scope - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).ScopeId = $policySet.scopeId - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PolicySetDisplayName = $policySet.PolicySetDisplayName - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PolicySetDefinitionName = $policySet.PolicySetDefinitionName - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PolicySetDefinitionId = $policySet.PolicySetDefinitionId - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PolicySetCategory = $policySet.PolicySetCategory - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).UniqueAssignmentsCount = $policySet.UniqueAssignmentsCount - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).UniqueAssignments = $policySet.UniqueAssignments - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PoliciesUsedCount = $policySet.PoliciesUsedCount - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).PoliciesUsed = $policySet.PoliciesUsed4JSON - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).CreatedOn = $policySet.CreatedOn - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).CreatedBy = $policySet.CreatedByJson - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).UpdatedOn = $policySet.UpdatedOn - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).UpdatedBy = $policySet.UpdatedByJson - $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()).JSON = $policySet.Json + $htPolicyAndPolicySet.PolicySet.($policySet.PolicySetDefinitionId.ToLower()) = [ordered]@{ + PolicySetType = $policySet.Type + ScopeMGLevel = $policySet.ScopeMGLevel + Scope = $policySet.Scope + ScopeId = $policySet.scopeId + PolicySetDisplayName = $policySet.PolicySetDisplayName + PolicySetDefinitionName = $policySet.PolicySetDefinitionName + PolicySetDefinitionId = $policySet.PolicySetDefinitionId + PolicySetCategory = $policySet.PolicySetCategory + UniqueAssignmentsCount = $policySet.UniqueAssignmentsCount + UniqueAssignments = $policySet.UniqueAssignments + PoliciesUsedCount = $policySet.PoliciesUsedCount + PoliciesUsed = $policySet.PoliciesUsed4JSON + CreatedOn = $policySet.CreatedOn + CreatedBy = $policySet.CreatedByJson + UpdatedOn = $policySet.UpdatedOn + UpdatedBy = $policySet.UpdatedByJson + JSON = $policySet.Json + } + } + foreach ($key in $htCacheAssignmentsPolicy.keys | Sort-Object) { + $htPolicyAndPolicySet.PolicyAssignment.($key.ToLower()) = $htCacheAssignmentsPolicy.($key).Assignment } Write-Host " Exporting PolicyAll JSON '$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json'" - $htPolicyAndPolicySet | ConvertTo-JSON -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json" -Encoding utf8 -Force + $htPolicyAndPolicySet | ConvertTo-Json -Depth 99 | Set-Content -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_PolicyAll.json" -Encoding utf8 -Force $endPolicyAllJSON = Get-Date - Write-Host "Creating PolicyAll JSON duration: $((NEW-TIMESPAN -Start $startPolicyAllJSON -End $endPolicyAllJSON).TotalSeconds) seconds" + Write-Host "Creating PolicyAll JSON duration: $((New-TimeSpan -Start $startPolicyAllJSON -End $endPolicyAllJSON).TotalSeconds) seconds" } function buildTree($mgId, $prnt) { $getMg = $htEntities.values.where( { $_.type -eq 'Microsoft.Management/managementGroups' -and $_.id -eq $mgId }) @@ -1845,6 +1854,11 @@ function cacheBuiltIn { $htCacheDefinitionsPolicySet = $using:htCacheDefinitionsPolicySet $htCacheDefinitionsRole = $using:htCacheDefinitionsRole $htRoleDefinitionIdsUsedInPolicy = $using:htRoleDefinitionIdsUsedInPolicy + $ValidPolicyEffects = $using:ValidPolicyEffects + $htHashesBuiltInPolicy = $using:htHashesBuiltInPolicy + #functions + $function:detectPolicyEffect = $using:funcDetectPolicyEffect + $function:getPolicyHash = $using:funcGetPolicyHash if ($builtInCapability -eq 'PolicyDefinitions') { $currentTask = 'Caching built-in Policy definitions' @@ -1888,34 +1902,12 @@ function cacheBuiltIn { else { $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Preview = $false } - #effects - if ($builtinPolicyDefinition.properties.parameters.effect.defaultvalue) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = $builtinPolicyDefinition.properties.parameters.effect.defaultvalue - if ($builtinPolicyDefinition.properties.parameters.effect.allowedValues) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = $builtinPolicyDefinition.properties.parameters.effect.allowedValues -join ',' - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = 'n/a' - } - else { - if ($builtinPolicyDefinition.properties.parameters.policyEffect.defaultValue) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = $builtinPolicyDefinition.properties.parameters.policyEffect.defaultvalue - if ($builtinPolicyDefinition.properties.parameters.policyEffect.allowedValues) { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = $builtinPolicyDefinition.properties.parameters.policyEffect.allowedValues -join ',' - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = 'n/a' - } - else { - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = $builtinPolicyDefinition.Properties.policyRule.then.effect - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = 'n/a' - $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - } + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $builtinPolicyDefinition + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues + $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue + #endregion effect $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).Json = $builtinPolicyDefinition if (-not [string]::IsNullOrEmpty($builtinPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { @@ -1935,7 +1927,25 @@ function cacheBuiltIn { else { $script:htCacheDefinitionsPolicy.(($builtinPolicyDefinition.Id).ToLower()).RoleDefinitionIds = 'n/a' } + + #hashes for parity builtin/custom + # $script:htHashesBuiltInPolicy.(($builtinPolicyDefinition.Id).ToLower()) = @{ + # policyRuleHash = getPolicyHash -object ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99) + # } + $policyRuleHash = (getPolicyHash -json ($builtinPolicyDefinition.properties.policyRule | ConvertTo-Json -Depth 99)) + if (-not $htHashesBuiltInPolicy.($policyRuleHash)) { + $script:htHashesBuiltInPolicy.($policyRuleHash) = @{ + Policies = [System.Collections.ArrayList]@() + } + $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) + } + else { + #Write-Host "$($builtinPolicyDefinition.name) $($policyRuleHash) already exists" + $null = $script:htHashesBuiltInPolicy.($policyRuleHash).Policies.Add(($builtinPolicyDefinition.Id).ToLower()) + #$htHashesBuiltInPolicy.($policyRuleHash).Policies.Count + } } + Write-Host " $($htHashesBuiltInPolicy.Keys.Count) unique Policy rule hashes for built-in Policy definitions" } if ($builtInCapability -eq 'PolicyDefinitionsStatic') { @@ -1981,34 +1991,12 @@ function cacheBuiltIn { else { $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Preview = $false } - #effects - if ($staticPolicyDefinition.properties.parameters.effect.defaultvalue) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = $staticPolicyDefinition.properties.parameters.effect.defaultvalue - if ($staticPolicyDefinition.properties.parameters.effect.allowedValues) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = $staticPolicyDefinition.properties.parameters.effect.allowedValues -join ',' - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = 'n/a' - } - else { - if ($staticPolicyDefinition.properties.parameters.policyEffect.defaultValue) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = $staticPolicyDefinition.properties.parameters.policyEffect.defaultvalue - if ($staticPolicyDefinition.properties.parameters.policyEffect.allowedValues) { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = $staticPolicyDefinition.properties.parameters.policyEffect.allowedValues -join ',' - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = 'n/a' - } - else { - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = $staticPolicyDefinition.Properties.policyRule.then.effect - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = 'n/a' - $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = 'n/a' - } - } + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $staticPolicyDefinition + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectDefaultValue = $htEffectDetected.defaultValue + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectAllowedValue = $htEffectDetected.allowedValues + $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).effectFixedValue = $htEffectDetected.fixedValue + #endregion effect $script:htCacheDefinitionsPolicy.(($staticPolicyDefinition.Id).ToLower()).Json = $staticPolicyDefinition if (-not [string]::IsNullOrEmpty($staticPolicyDefinition.properties.policyRule.then.details.roleDefinitionIds)) { @@ -2147,7 +2135,7 @@ function checkAzGovVizVersion { $script:azGovVizNewerVersionAvailable = $false if ([int]$azGovVizVersionOnRepository -gt [int]$azGovVizVersionThis) { $script:azGovVizNewerVersionAvailable = $true - $script:azGovVizNewerVersionAvailableHTML = 'Get the latest AzGovViz version (' + $azGovVizVersionOnRepositoryFull + ')! ' + $script:azGovVizNewerVersionAvailableHTML = 'Get the latest Azure Governance Visualizer version (' + $azGovVizVersionOnRepositoryFull + ')! ' } } catch { @@ -2258,7 +2246,7 @@ function detailSubscriptions { }) } else { - #Write-Host " preCustomDataCollection: $($childrenSubscription.properties.displayName) ($($childrenSubscription.name)) Subscription Quota Id: $($sub.subDetails.subscriptionPolicies.quotaId) is out of scope for AzGovViz (not in Whitelist)" + #Write-Host " preCustomDataCollection: $($childrenSubscription.properties.displayName) ($($childrenSubscription.name)) Subscription Quota Id: $($sub.subDetails.subscriptionPolicies.quotaId) is out of scope for Azure Governance Visualizer (not in Whitelist)" $null = $script:outOfScopeSubscriptions.Add([PSCustomObject]@{ subscriptionId = $childrenSubscription.name subscriptionName = $childrenSubscription.properties.displayName @@ -2313,6 +2301,90 @@ function detailSubscriptions { $end = Get-Date Write-Host "Subscription picking duration: $((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds" } +function detectPolicyEffect { + [CmdletBinding()] + Param + ( + [object] + $policyDefinition + ) + + $htEffect = @{ + defaultValue = 'n/a' + allowedValues = 'n/a' + fixedValue = 'n/a' + } + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.policyRule.then.effect)) { + if ($policyDefinition.properties.policyRule.then.effect -in $ValidPolicyEffects) { + # $arrayeffect += "fixed: $($policyDefinition.properties.policyRule.then.effect)" + # return $arrayeffect + $htEffect.fixedValue = $policyDefinition.properties.policyRule.then.effect + return $htEffect + } + else { + $Regex = [Regex]::new("(?<=\[parameters\(')(.*)(?='\)\])") + $Match = $Regex.Match($policyDefinition.properties.policyRule.then.effect) + if ($Match.Success) { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value))) { + + #defaultValue + if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'defaultvalue') { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).defaultValue)) { + if ($policyDefinition.properties.parameters.($Match.Value).defaultValue -in $ValidPolicyEffects) { + #$arrayeffect += "default: $($policyDefinition.properties.parameters.($Match.Value).defaultValue)" + $htEffect.defaultValue = $policyDefinition.properties.parameters.($Match.Value).defaultValue + } + else { + Write-Host "invalid defaultValue effect $($policyDefinition.properties.parameters.($Match.Value).defaultValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "defaultValue empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "no defaultvalue - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + #allowedValues + if (($policyDefinition.properties.parameters.($Match.Value) | Get-Member).name -contains 'allowedValues') { + if (-not [string]::IsNullOrWhiteSpace($policyDefinition.properties.parameters.($Match.Value).allowedValues)) { + if ($policyDefinition.properties.parameters.($Match.Value).allowedValues.Count -gt 0) { + #Write-Host "allowedValues count $($policyDefinition.properties.parameters.($Match.Value).allowedValues) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + $arrayAllowed = @() + foreach ($allowedValue in $policyDefinition.properties.parameters.($Match.Value).allowedValues) { + if ($allowedValue -in $ValidPolicyEffects) { + $arrayAllowed += $allowedValue + } + else { + Write-Host "invalid allowedValue effect $($allowedValue) - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + #$arrayeffect += "allowed: $(($arrayAllowed | Sort-Object) -join ', ')" + $htEffect.allowedValues = ($arrayAllowed | Sort-Object) -join ',' + } + } + else { + Write-Host "allowedValues empty - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + } + else { + Write-Host "no allowedValues- $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + + } + else { + Write-Host "unexpected - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + + return $htEffect + } + } + } + else { + Write-Host "no then effect - $($policyDefinition.name) ($($policyDefinition.properties.policyType))" + } + return $htEffect +} function exportBaseCSV { Write-Host "Exporting CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName).csv'" $startBuildCSV = Get-Date @@ -2433,8 +2505,9 @@ function getConsumption { foreach ($consumptionline in $consumptiondataFromAPI.properties.rows) { $hlper = $htSubscriptionsMgPath.($consumptionline[1]) + $null = $script:allConsumptionData.Add([PSCustomObject]@{ - "$($consumptiondataFromAPI.properties.columns.name[0])" = $consumptionline[0] + "$($consumptiondataFromAPI.properties.columns.name[0])" = [decimal]$consumptionline[0] "$($consumptiondataFromAPI.properties.columns.name[1])" = $consumptionline[1] SubscriptionName = $hlper.DisplayName SubscriptionMgPath = $hlper.ParentNameChainDelimited @@ -3383,7 +3456,7 @@ function getMDfCSecureScoreMG { } function getOrphanedResources { $start = Get-Date - Write-Host 'Getting orphaned resources (ARG)' + Write-Host 'Getting orphaned/unused resources (ARG)' $queries = [System.Collections.ArrayList]@() $intent = 'cost savings - stopped but not deallocated VM' @@ -3560,16 +3633,16 @@ function getOrphanedResources { } } - Write-Host " Found $($arrayOrphanedResources.Count) orphaned Resources" - Write-Host " Exporting OrphanedResources CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesOrphaned.csv'" - $arrayOrphanedResources | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesOrphaned.csv" -Delimiter "$csvDelimiter" -NoTypeInformation + Write-Host " Found $($arrayOrphanedResources.Count) orphaned/unused Resources" + Write-Host " Exporting OrphanedResources CSV '$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv'" + $arrayOrphanedResources | Sort-Object -Property Resource | Export-Csv -Path "$($outputPath)$($DirectorySeparatorChar)$($fileName)_ResourcesCostOptimizationAndCleanup.csv" -Delimiter "$csvDelimiter" -NoTypeInformation } else { - Write-Host ' No orphaned Resources found' + Write-Host ' No orphaned/unused Resources found' } $end = Get-Date - Write-Host "Getting orphaned resources (ARG) processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" + Write-Host "Getting orphaned/unused resources (ARG) processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" } function getPIMEligible { $start = Get-Date @@ -3780,6 +3853,15 @@ function getPIMEligible { $end = Get-Date Write-Host "Getting PIM Eligible assignments processing duration: $((New-TimeSpan -Start $start -End $end).TotalMinutes) minutes ($((New-TimeSpan -Start $start -End $end).TotalSeconds) seconds)" } +function getPolicyHash { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $json + ) + return [System.BitConverter]::ToString([System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($json))) +} function getResourceDiagnosticsCapability { Write-Host 'Checking Resource Types Diagnostics capability (1st party only)' $startResourceDiagnosticsCheck = Get-Date @@ -3922,16 +4004,8 @@ function getSubscriptions { Write-Host " $($requestAllSubscriptionsAPI.Count) Subscriptions returned" foreach ($subscription in $requestAllSubscriptionsAPI) { - # #test - # if ($subscription.subscriptionId -eq 'GUID') { - # Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - AzGovViz: excluding this Subscripion" -ForegroundColor DarkRed - # $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId) = @{} - # $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId).subDetails = $subscription - # continue - # } - if ($subscription.tenantId -ne $azAPICallConf['checkcontext'].tenant.id) { - Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - AzGovViz: excluding this Subscripion" -ForegroundColor DarkRed + Write-Host " Finding: $($subscription.displayName) ($($subscription.subscriptionId)) belongs to foreign tenant '$($subscription.tenantId)' - Azure Governance Visualizer: excluding this Subscripion" -ForegroundColor DarkRed $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId) = @{} $script:htSubscriptionsFromOtherTenants.($subscription.subscriptionId).subDetails = $subscription } @@ -4936,6 +5010,7 @@ function processDataCollection { $alzPolicyHashes = $using:alzPolicyHashes $alzPolicySetHashes = $using:alzPolicySetHashes $htDoARMRoleAssignmentScheduleInstances = $using:htDoARMRoleAssignmentScheduleInstances + $ValidPolicyEffects = $using:ValidPolicyEffects #other $function:addRowToTable = $using:funcAddRowToTable $function:namingValidation = $using:funcNamingValidation @@ -4952,6 +5027,7 @@ function processDataCollection { $function:dataCollectionPolicyAssignmentsMG = $using:funcDataCollectionPolicyAssignmentsMG $function:dataCollectionRoleDefinitions = $using:funcDataCollectionRoleDefinitions $function:dataCollectionRoleAssignmentsMG = $using:funcDataCollectionRoleAssignmentsMG + $function:detectPolicyEffect = $using:funcDetectPolicyEffect #endregion usingVARS $builtInPolicyDefinitionsCount = $using:builtInPolicyDefinitionsCount @@ -5104,14 +5180,6 @@ function processDataCollection { #region SUBSCRIPTION Write-Host ' CustomDataCollection Subscriptions' - # $subsExcludedStateCount = ($outOfScopeSubscriptions.where( { $_.outOfScopeReason -like 'State*' } )).Count - # $subsExcludedWhitelistCount = ($outOfScopeSubscriptions.where( { $_.outOfScopeReason -like 'QuotaId*' } )).Count - # if ($subsExcludedStateCount -gt 0) { - # Write-Host " CustomDataCollection $($subsExcludedStateCount) Subscriptions excluded (State != enabled)" - # } - # if ($subsExcludedWhitelistCount -gt 0) { - # Write-Host " CustomDataCollection $($subsExcludedWhitelistCount) Subscriptions excluded (not in quotaId whitelist: '$($SubscriptionQuotaIdWhitelist -join ', ')' OR is AAD_ quotaId)" - # } if ($outOfScopeSubscriptions.Count -gt 0) { Write-Host " CustomDataCollection $($outOfScopeSubscriptions.Count) Subscriptions excluded" -ForegroundColor yellow @@ -5219,6 +5287,7 @@ function processDataCollection { $htResourcePropertiesConvertfromJSONFailed = $using:htResourcePropertiesConvertfromJSONFailed $htAvailablePrivateEndpointTypes = $using:htAvailablePrivateEndpointTypes $arrayAdvisorScores = $using:arrayAdvisorScores + $ValidPolicyEffects = $using:ValidPolicyEffects #$htResourcesWithProperties = $using:htResourcesWithProperties #other $function:addRowToTable = $using:funcAddRowToTable @@ -5250,6 +5319,7 @@ function processDataCollection { $function:dataCollectionVNets = $using:funcDataCollectionVNets $function:dataCollectionPrivateEndpoints = $using:funcDataCollectionPrivateEndpoints $function:dataCollectionAdvisorScores = $using:funcDataCollectionAdvisorScores + $function:detectPolicyEffect = $using:funcDetectPolicyEffect #endregion UsingVARs $addRowToTableDone = $false @@ -9942,10 +10012,10 @@ btn_reset: true, highlight_keywords: true, alternate_rows: true, auto_filter: { $htmlTableId = "ScopeInsights_OrphanedResources_$($subscriptionId -replace '-','_')" $randomFunctionName = "func_$htmlTableId" [void]$htmlScopeInsights.AppendLine(@" - +

   'Azure Orphan Resources' ARG queries and workbooks GitHub
-   Resource details can be found in the CSV output *_ResourcesOrphaned.csv
+   Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
   Download CSV semicolon | comma @@ -10052,13 +10122,13 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Orphaned Resources + No cost optimization & cleanup '@) } } else { [void]$htmlScopeInsights.AppendLine(@' - 0 Orphaned Resources + No cost optimization & cleanup '@) } [void]$htmlScopeInsights.AppendLine(@' @@ -12131,7 +12201,7 @@ function processStorageAccountAnalysis { foreach ($sa in $saConsumptionByResourceId) { $htSACost.($sa.Name) = @{} $htSACost.($sa.Name).meterCategoryAll = ($sa.Group.MeterCategory | Sort-Object) -join ', ' - $htSACost.($sa.Name).costAll = [decimal]($sa.Group.PreTaxCost | Measure-Object -Sum).Sum + $htSACost.($sa.Name).costAll = ($sa.Group.PreTaxCost | Measure-Object -Sum).Sum #[decimal]($sa.Group.PreTaxCost | Measure-Object -Sum).Sum $htSACost.($sa.Name).currencyAll = ($sa.Group.Currency | Sort-Object -Unique) -join ', ' foreach ($costentry in $sa.Group) { $htSACost.($sa.Name)."cost_$($costentry.MeterCategory)" = $costentry.PreTaxCost @@ -12804,9 +12874,9 @@ function processTenantSummary() { AssignmentType = 'indirect' AssignmentInheritFrom = "$($rbac.RoleAssignmentIdentityDisplayname) ($($rbac.RoleAssignmentIdentityObjectId))" GroupMembersCount = "$($grpHlpr.MembersAllCount) (Usr: $($grpHlpr.MembersUsersCount)$($CsvDelimiterOpposite) Grp: $($grpHlpr.MembersGroupsCount)$($CsvDelimiterOpposite) SP: $($grpHlpr.MembersServicePrincipalsCount))" - ObjectDisplayName = "AzGovViz:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" - ObjectSignInName = "AzGovViz:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" - ObjectId = "AzGovViz:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectDisplayName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectSignInName = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" + ObjectId = "Azure Governance Visualizer:TooManyMembers ($($htAADGroupsDetails.($rbac.RoleAssignmentIdentityObjectId).MembersAllCount))" ObjectType = 'unresolved' RbacRelatedPolicyAssignment = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignment RbacRelatedPolicyAssignmentClear = $hlpRoleAssignmentRelatedPolicyAssignments.relatedPolicyAssignmentClear @@ -13268,7 +13338,7 @@ function processTenantSummary() { } if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { - Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by AzGovViz - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow + Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow } } } @@ -13364,9 +13434,12 @@ function processTenantSummary() { if ($tenantPolicy.effectDefaultValue -ne 'n/a') { $effect = "Default: $($tenantPolicy.effectDefaultValue); Allowed: $($tenantPolicy.effectAllowedValue)" } - else { + elseif ($tenantPolicy.effectFixedValue -ne 'n/a') { $effect = "Fixed: $($tenantPolicy.effectFixedValue)" } + else { + $effect = 'n/a' + } if (($tenantPolicy.RoleDefinitionIds) -ne 'n/a') { $policyRoleDefinitionsArray = @() @@ -14822,6 +14895,110 @@ extensions: [{ name: 'sort' }] } #endregion SUMMARYCustompolicySetOrphandedTenantRoot + #region SUMMARYPolicyParityCustomBuiltIn + Write-Host ' processing TenantSummary Policy parity custom built-in' + + if ($arrayCustomBuiltInPolicyParity.Count -gt 0) { + $tfCount = $arrayCustomBuiltInPolicyParity.Count + $htmlTableId = 'TenantSummary_PolicyCustomBuiltInParity' + [void]$htmlTenantSummary.AppendLine(@" + +
+ Download CSV semicolon | comma +
+ + + + + + + + + + + +"@) + + $htmlSUMMARYPolicyCustomBuiltInParity = $null + $htmlSUMMARYPolicyCustomBuiltInParity = foreach ($entry in $arrayCustomBuiltInPolicyParity | Sort-Object -Property CustomPolicyId) { + $arrayBuiltinsRef = @() + foreach ($builtInPolicyId in $entry.BuiltInPolicyId) { + $arrayBuiltinsRef += "$($htCacheDefinitionsPolicy.($builtInPolicyId).DisplayName) ($($builtInPolicyId -replace '.*/'))" + } + $builtInPolicyAzA = $arrayBuiltinsRef -join ', ' + @" + + + + + + + + +"@ + } + + [void]$htmlTenantSummary.AppendLine($htmlSUMMARYPolicyCustomBuiltInParity) + [void]$htmlTenantSummary.AppendLine(@" + +
Policy NamePolicy DisplayNamePolicy CategoryPolicy Id# match built-inBuilt-In Policy
$($entry.CustomPolicyName)$($entry.CustomPolicyDisplayName)$($entry.CustomPolicyCategory)$($entry.CustomPolicyId)$($entry.MatchBuiltinPolicyCount)$($builtInPolicyAzA)
+
+ +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

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

+'@) + } + #endregion SUMMARYPolicyParityCustomBuiltIn + #region SUMMARYALZPolicies Write-Host ' processing TenantSummary ALZPolicies' @@ -15084,7 +15261,7 @@ extensions: [{ name: 'sort' }] $tfCount = ($policySetsDeprecated).count $htmlTableId = 'TenantSummary_policySetsDeprecated' [void]$htmlTenantSummary.AppendLine(@" -
Download CSV semicolon | comma @@ -15172,7 +15349,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYPolicySetsDeprecatedPolicy @@ -15233,7 +15410,7 @@ extensions: [{ name: 'sort' }] $tfCount = ($policyAssignmentsDeprecated).count $htmlTableId = 'TenantSummary_policyAssignmentsDeprecated' [void]$htmlTenantSummary.AppendLine(@" -
Download CSV semicolon | comma @@ -15324,7 +15501,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYPolicyAssignmentsDeprecatedPolicy @@ -16360,7 +16537,7 @@ extensions: [{ name: 'sort' }] } if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { - Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by AzGovViz - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow + Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow } } } @@ -16426,7 +16603,7 @@ extensions: [{ name: 'sort' }] Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -17381,7 +17558,7 @@ extensions: [{ name: 'sort' }] Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -17808,7 +17985,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No PIM Eligibility - run AzGovViz with a Service Principal to get PIM Eligibility insights

+

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

'@) } } @@ -19912,7 +20089,7 @@ extensions: [{ name: 'sort' }] #region SUMMARYOrphanedResources $startSUMMARYOrphanedResources = Get-Date - Write-Host ' processing TenantSummary Orphaned Resources' + Write-Host ' processing TenantSummary Orphaned/unused Resources' if ($arrayOrphanedResources.count -gt 0) { $script:arrayOrphanedResourcesSlim = $arrayOrphanedResources | Sort-Object -Property type @@ -19937,11 +20114,11 @@ extensions: [{ name: 'sort' }] $tfCount = $orphanedResourceTypesCount $htmlTableId = 'TenantSummary_orphanedResources' [void]$htmlTenantSummary.AppendLine(@" -
'Azure Orphan Resources' ARG queries and workbooks GitHub
- Resource details can be found in the CSV output *_ResourcesOrphaned.csv
+ Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
Download CSV semicolon | comma @@ -20056,11 +20233,11 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Orphaned Resources

+

No cost optimization & cleanup

'@) } $endSUMMARYOrphanedResources = Get-Date - Write-Host " SUMMARY Orphaned Resources processing duration: $((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalSeconds) seconds)" + Write-Host " SUMMARY Orphaned/unused Resources processing duration: $((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalSeconds) seconds)" #endregion SUMMARYOrphanedResources #region SUMMARYSubResourceProviders @@ -20359,7 +20536,7 @@ extensions: [{ name: 'sort' }]
Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -23142,7 +23319,7 @@ extensions: [{ name: 'sort' }] } } else { - $status = 'AzGovViz did not detect the resourceType' + $status = 'Azure Governance Visualizer did not detect the resourceType' $diagnosticsLogCategoriesSupported = 'n/a' $diagnosticsLogCategoriesNotCoveredByPolicy = 'n/a' $recommendation = 'no recommendation as this resourceType seems not existing' @@ -27385,14 +27562,14 @@ function runInfo { $script:paramsUsed += 'SubscriptionQuotaIdWhitelist: false ' } else { - Write-Host ' Subscription Whitelist enabled. AzGovViz will only process Subscriptions where QuotaId startswith one of the following strings:' -ForegroundColor Green + Write-Host ' Subscription Whitelist enabled. Azure Governance Visualizer will only process Subscriptions where QuotaId startswith one of the following strings:' -ForegroundColor Green foreach ($quotaIdFromSubscriptionQuotaIdWhitelist in $SubscriptionQuotaIdWhitelist) { Write-Host " - $($quotaIdFromSubscriptionQuotaIdWhitelist)" -ForegroundColor Green } foreach ($whiteListEntry in $SubscriptionQuotaIdWhitelist) { if ($whiteListEntry -eq 'undefined') { Write-Host "When defining the 'SubscriptionQuotaIdWhitelist' make sure to remove the 'undefined' entry from the array :)" -ForegroundColor Red - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } } $script:paramsUsed += "SubscriptionQuotaIdWhitelist: $($SubscriptionQuotaIdWhitelist -join ', ') " @@ -27466,11 +27643,11 @@ function runInfo { if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { if (-not $AzureConsumptionPeriod -is [int]) { Write-Host 'parameter -AzureConsumptionPeriod must be an integer' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } elseif ($AzureConsumptionPeriod -eq 0) { Write-Host 'parameter -AzureConsumptionPeriod must be gt 0' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } else { #$azureConsumptionStartDate = ((Get-Date).AddDays( - ($($AzureConsumptionPeriod)))).ToString("yyyy-MM-dd") @@ -28306,12 +28483,12 @@ function validateAccess { } if ($permissionsCheckFailed -eq $true) { Write-Host "Please consult the documentation: https://$($GithubRepository)#required-permissions-in-azure" - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } if ($getAzManagementGroups.Count -eq 0) { Write-Host 'Management Groups count returned null' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } else { Write-Host "Detected $($getAzManagementGroups.Count) Management Groups" @@ -28320,7 +28497,7 @@ function validateAccess { [array]$MgtGroupArray = addIndexNumberToArray -array ($getAzManagementGroups) if (-not $MgtGroupArray) { Write-Host 'Seems you do not have access to any Management Group. Please make sure you have the required RBAC role [Reader] assigned on at least one Management Group' -ForegroundColor Red - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } selectMg @@ -28365,7 +28542,7 @@ function validateAccess { if ($permissionsCheckFailed -eq $true) { Write-Host "Please consult the documentation for permission requirements: https://$($GithubRepository)#technical-documentation" - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } } @@ -29048,7 +29225,7 @@ function dataCollectionResources { } } else { - Write-Host "[AzGovViz] Please file an issue at the AzGovViz GitHub repository (aka.ms/AzGovViz) and provide this information (scrub subscription Id and company identifyable names): No API-version matches! ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)' - Thank you!" -ForegroundColor DarkRed + Write-Host "[Azure Governance Visualizer] Please file an issue at the Azure Governance Visualizer GitHub repository (aka.ms/AzGovViz) and provide this information (scrub subscription Id and company identifyable names): No API-version matches! ResourceType: '$($resource.type)'; ResourceId: '$($resource.id)' - Thank you!" -ForegroundColor DarkRed } } else { @@ -30638,34 +30815,13 @@ function dataCollectionPolicyDefinitions { else { $htTemp.Preview = $false } - #effects - if ($scopePolicyDefinition.properties.parameters.effect.defaultvalue) { - $htTemp.effectDefaultValue = $scopePolicyDefinition.properties.parameters.effect.defaultvalue - if ($scopePolicyDefinition.properties.parameters.effect.allowedValues) { - $htTemp.effectAllowedValue = $scopePolicyDefinition.properties.parameters.effect.allowedValues -join ',' - } - else { - $htTemp.effectAllowedValue = 'n/a' - } - $htTemp.effectFixedValue = 'n/a' - } - else { - if ($scopePolicyDefinition.properties.parameters.policyEffect.defaultValue) { - $htTemp.effectDefaultValue = $scopePolicyDefinition.properties.parameters.policyEffect.defaultvalue - if ($scopePolicyDefinition.properties.parameters.policyEffect.allowedValues) { - $htTemp.effectAllowedValue = $scopePolicyDefinition.properties.parameters.policyEffect.allowedValues -join ',' - } - else { - $htTemp.effectAllowedValue = 'n/a' - } - $htTemp.effectFixedValue = 'n/a' - } - else { - $htTemp.effectFixedValue = $scopePolicyDefinition.Properties.policyRule.then.effect - $htTemp.effectDefaultValue = 'n/a' - $htTemp.effectAllowedValue = 'n/a' - } - } + + #region effect + $htEffectDetected = detectPolicyEffect -policyDefinition $scopePolicyDefinition + $htTemp.effectDefaultValue = $htEffectDetected.defaultValue + $htTemp.effectAllowedValue = $htEffectDetected.allowedValues + $htTemp.effectFixedValue = $htEffectDetected.fixedValue + #endregion effect $htTemp.Json = $scopePolicyDefinition $script:htCacheDefinitionsPolicy.($hlpPolicyDefinitionId) = $htTemp @@ -31094,7 +31250,7 @@ function dataCollectionPolicyAssignmentsMG { foreach ($tmpPolicyDefinitionId in ($($htCacheDefinitionsPolicy).Keys | Sort-Object)) { Write-Host $tmpPolicyDefinitionId } - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } } #policyDefinition Scope does not exist @@ -33036,6 +33192,8 @@ $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 @@ -33058,7 +33216,7 @@ if ($DoPSRule) { Write-Host '' Write-Host ' * * * CHANGE: PSRule for Azure * * *' -ForegroundColor Magenta Write-Host 'PSRule integration has been paused' - Write-Host 'AzGovViz 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.' + 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 '' @@ -33110,8 +33268,8 @@ checkAzGovVizVersion if ($azGovVizNewerVersionAvailable) { if (-not $azAPICallConf['htParameters'].onAzureDevOpsOrGitHubActions) { Write-Host '' - Write-Host " * * * This AzGovViz version ($ProductVersion) is not up to date. Get the latest AzGovViz version ($azGovVizVersionOnRepositoryFull)! * * *" -ForegroundColor Green - Write-Host 'Check the AzGovViz history: https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting/blob/master/history.md' + 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/JulianHayward/Azure-MG-Sub-Governance-Reporting/blob/master/history.md' Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor Green Pause } @@ -33131,7 +33289,7 @@ if (-not $HierarchyMapOnly) { 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 AzGovViz with the parameter -DoPSRule (example: .\pwsh\AzGovVizParallel.ps1 -DoPSRule)' + Write-Host 'Consider running Azure Governance Visualizer with the parameter -DoPSRule (example: .\pwsh\AzGovVizParallel.ps1 -DoPSRule)' Write-Host ' * * * * * * * * * * * * * * * * * * * * * *' -ForegroundColor Magenta Pause } @@ -33180,7 +33338,7 @@ else { getFileNaming -Write-Host "Running AzGovViz for ManagementGroupId: '$ManagementGroupId'" -ForegroundColor Yellow +Write-Host "Running Azure Governance Visualizer for ManagementGroupId: '$ManagementGroupId'" -ForegroundColor Yellow $newTable = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) $htMgDetails = @{} @@ -33301,6 +33459,8 @@ if (-not $HierarchyMapOnly) { $htResourceProvidersRef = @{} $htAvailablePrivateEndpointTypes = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} $arrayAdvisorScores = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) + $htHashesBuiltInPolicy = [System.Collections.Hashtable]::Synchronized((New-Object System.Collections.Hashtable)) #@{} + $arrayCustomBuiltInPolicyParity = [System.Collections.ArrayList]@() } if (-not $HierarchyMapOnly) { @@ -33622,6 +33782,35 @@ if (-not $HierarchyMapOnly) { $tenantBuiltInPoliciesCount = ($tenantBuiltInPolicies).count $tenantCustomPolicies = (($htCacheDefinitionsPolicy).Values).where({ $_.Type -eq 'Custom' } ) $tenantCustomPoliciesCount = ($tenantCustomPolicies).count + + #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 @@ -33974,7 +34163,7 @@ $html = @" - AzGovViz + Azure Governance Visualizer aka AzGovViz - + - + - + - + - + - + +"@) + } + else { + [void]$htmlTenantSummary.AppendLine(@' +

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

+'@) + } + #endregion SUMMARYPolicyParityCustomBuiltIn + #region SUMMARYALZPolicies Write-Host ' processing TenantSummary ALZPolicies' @@ -2627,7 +2734,7 @@ extensions: [{ name: 'sort' }] $tfCount = ($policySetsDeprecated).count $htmlTableId = 'TenantSummary_policySetsDeprecated' [void]$htmlTenantSummary.AppendLine(@" -
Download CSV semicolon | comma @@ -2715,7 +2822,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYPolicySetsDeprecatedPolicy @@ -2776,7 +2883,7 @@ extensions: [{ name: 'sort' }] $tfCount = ($policyAssignmentsDeprecated).count $htmlTableId = 'TenantSummary_policyAssignmentsDeprecated' [void]$htmlTenantSummary.AppendLine(@" -
Download CSV semicolon | comma @@ -2867,7 +2974,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@" -

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

+

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

"@) } #endregion SUMMARYPolicyAssignmentsDeprecatedPolicy @@ -3903,7 +4010,7 @@ extensions: [{ name: 'sort' }] } if ($resolvedIdentity.'@odata.type' -ne '#microsoft.graph.user' -and $resolvedIdentity.'@odata.type' -ne '#microsoft.graph.servicePrincipal') { - Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by AzGovViz - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow + Write-Host "!!! * * * IdentityType '$($resolvedIdentity.'@odata.type')' was not considered by Azure Governance Visualizer - if you see this line, please file an issue on GitHub - thank you." -ForegroundColor Yellow } } } @@ -3969,7 +4076,7 @@ extensions: [{ name: 'sort' }] Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -4924,7 +5031,7 @@ extensions: [{ name: 'sort' }] Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
You can reduce the number of lines by using parameter -LargeTenant and/or -DoNotIncludeResourceGroupsAndResourcesOnRBAC
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -5351,7 +5458,7 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No PIM Eligibility - run AzGovViz with a Service Principal to get PIM Eligibility insights

+

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

'@) } } @@ -7455,7 +7562,7 @@ extensions: [{ name: 'sort' }] #region SUMMARYOrphanedResources $startSUMMARYOrphanedResources = Get-Date - Write-Host ' processing TenantSummary Orphaned Resources' + Write-Host ' processing TenantSummary Orphaned/unused Resources' if ($arrayOrphanedResources.count -gt 0) { $script:arrayOrphanedResourcesSlim = $arrayOrphanedResources | Sort-Object -Property type @@ -7480,11 +7587,11 @@ extensions: [{ name: 'sort' }] $tfCount = $orphanedResourceTypesCount $htmlTableId = 'TenantSummary_orphanedResources' [void]$htmlTenantSummary.AppendLine(@" -
'Azure Orphan Resources' ARG queries and workbooks GitHub
- Resource details can be found in the CSV output *_ResourcesOrphaned.csv
+ Resource details can be found in the CSV output *_ResourcesCostOptimizationAndCleanup.csv
Download CSV semicolon | comma
@@ -7599,11 +7706,11 @@ extensions: [{ name: 'sort' }] } else { [void]$htmlTenantSummary.AppendLine(@' -

No Orphaned Resources

+

No cost optimization & cleanup

'@) } $endSUMMARYOrphanedResources = Get-Date - Write-Host " SUMMARY Orphaned Resources processing duration: $((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalSeconds) seconds)" + Write-Host " SUMMARY Orphaned/unused Resources processing duration: $((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalMinutes) minutes ($((New-TimeSpan -Start $startSUMMARYOrphanedResources -End $endSUMMARYOrphanedResources).TotalSeconds) seconds)" #endregion SUMMARYOrphanedResources #region SUMMARYSubResourceProviders @@ -7902,7 +8009,7 @@ extensions: [{ name: 'sort' }]
Output of $tfCount lines would exceed the html rows limit of $HtmlTableRowsLimit (html file potentially would become unresponsive). Work with the CSV file $($csvFilename).csv | Note: the CSV file will only exist if you did NOT use parameter -NoCsvExport
You can adjust the html row limit by using parameter -HtmlTableRowsLimit
- Check the parameters documentation AzGovViz docs + Check the parameters documentation Azure Governance Visualizer docs
"@) } @@ -10685,7 +10792,7 @@ extensions: [{ name: 'sort' }] } } else { - $status = 'AzGovViz did not detect the resourceType' + $status = 'Azure Governance Visualizer did not detect the resourceType' $diagnosticsLogCategoriesSupported = 'n/a' $diagnosticsLogCategoriesNotCoveredByPolicy = 'n/a' $recommendation = 'no recommendation as this resourceType seems not existing' diff --git a/pwsh/dev/functions/runInfo.ps1 b/pwsh/dev/functions/runInfo.ps1 index 4fb5cda7..49da6ac5 100644 --- a/pwsh/dev/functions/runInfo.ps1 +++ b/pwsh/dev/functions/runInfo.ps1 @@ -37,14 +37,14 @@ function runInfo { $script:paramsUsed += 'SubscriptionQuotaIdWhitelist: false ' } else { - Write-Host ' Subscription Whitelist enabled. AzGovViz will only process Subscriptions where QuotaId startswith one of the following strings:' -ForegroundColor Green + Write-Host ' Subscription Whitelist enabled. Azure Governance Visualizer will only process Subscriptions where QuotaId startswith one of the following strings:' -ForegroundColor Green foreach ($quotaIdFromSubscriptionQuotaIdWhitelist in $SubscriptionQuotaIdWhitelist) { Write-Host " - $($quotaIdFromSubscriptionQuotaIdWhitelist)" -ForegroundColor Green } foreach ($whiteListEntry in $SubscriptionQuotaIdWhitelist) { if ($whiteListEntry -eq 'undefined') { Write-Host "When defining the 'SubscriptionQuotaIdWhitelist' make sure to remove the 'undefined' entry from the array :)" -ForegroundColor Red - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } } $script:paramsUsed += "SubscriptionQuotaIdWhitelist: $($SubscriptionQuotaIdWhitelist -join ', ') " @@ -118,11 +118,11 @@ function runInfo { if ($azAPICallConf['htParameters'].DoAzureConsumption -eq $true) { if (-not $AzureConsumptionPeriod -is [int]) { Write-Host 'parameter -AzureConsumptionPeriod must be an integer' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } elseif ($AzureConsumptionPeriod -eq 0) { Write-Host 'parameter -AzureConsumptionPeriod must be gt 0' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } else { #$azureConsumptionStartDate = ((Get-Date).AddDays( - ($($AzureConsumptionPeriod)))).ToString("yyyy-MM-dd") diff --git a/pwsh/dev/functions/validateAccess.ps1 b/pwsh/dev/functions/validateAccess.ps1 index eda0adf3..2ce4099f 100644 --- a/pwsh/dev/functions/validateAccess.ps1 +++ b/pwsh/dev/functions/validateAccess.ps1 @@ -89,12 +89,12 @@ function validateAccess { } if ($permissionsCheckFailed -eq $true) { Write-Host "Please consult the documentation: https://$($GithubRepository)#required-permissions-in-azure" - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } if ($getAzManagementGroups.Count -eq 0) { Write-Host 'Management Groups count returned null' - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } else { Write-Host "Detected $($getAzManagementGroups.Count) Management Groups" @@ -103,7 +103,7 @@ function validateAccess { [array]$MgtGroupArray = addIndexNumberToArray -array ($getAzManagementGroups) if (-not $MgtGroupArray) { Write-Host 'Seems you do not have access to any Management Group. Please make sure you have the required RBAC role [Reader] assigned on at least one Management Group' -ForegroundColor Red - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } selectMg @@ -148,7 +148,7 @@ function validateAccess { if ($permissionsCheckFailed -eq $true) { Write-Host "Please consult the documentation for permission requirements: https://$($GithubRepository)#technical-documentation" - Throw 'Error - AzGovViz: check the last console output for details' + Throw 'Error - Azure Governance Visualizer: check the last console output for details' } } diff --git a/setup.md b/setup.md index 43be3bad..fbc76da6 100644 --- a/setup.md +++ b/setup.md @@ -1,65 +1,78 @@ -# AzGovViz - Azure Governance Visualizer - Setup +# Azure Governance Visualizer aka AzGovViz - Setup This guide will help you to setup and run AzGovViz * Abbreviations: * Azure Active Directory - AAD * Azure DevOps - AzDO - -## Table of contents - -* [__AzGovViz from Console__](#azgovviz-from-console) - * Grant permissions in Azure - * Execution options - * Option 1 - Execute as a Tenant Member User - * Option 2 - Execute as a Tenant Guest User - * Option 3 - Execute as Service Principal - * Clone the AzGovViz repository - * Run AzGovViz - -* [__AzGovViz in Azure DevOps (AzDO)__](#azgovviz-in-azure-devops) - * Create AzDO Project - * Import AzGovViz GitHub repository - * Create AzDO Service Connection - * Option 1 - Create Service Connection in AzDO - * Option 2 - Create Service Connection´s Service Principal in the Azure Portal - * Grant permissions in Azure - * Grant permissions in AAD - * Grant permissions on AzGovViz AzDO repository - * Edit AzDO YAML file - * Create AzDO Pipeline - * Run the AzDO Pipeline - * Create AzDO Wiki - WikiAsCode - -* [__AzGovViz in GitHub Actions__](#azgovviz-in-github-actions) - * Create GitHub repository - * Import Code - * AzGovViz YAML - * Store the credentials in GitHub - * Edit the workflow YAML file - * Run AzGovViz in GitHub Actions - * AzGovViz OIDC YAML - * Store the credentials in GitHub - * Edit the workflow YAML file - * Run AzGovViz in GitHub Actions - -* [__AzGovViz in GitHub Codespaces__](#azgovviz-github-codespaces) - -* [__Optional Publishing the AzGovViz HTML to a Azure Web App__](#optional-publishing-the-azgovviz-html-to-a-azure-web-app) - -# AzGovViz from Console +# Table of content +- [Azure Governance Visualizer aka AzGovViz - Setup](#azure-governance-visualizer-aka-azgovviz---setup) +- [Table of content](#table-of-content) +- [Azure Governance Visualizer from Console](#azure-governance-visualizer-from-console) + - [Grant permissions in Azure](#grant-permissions-in-azure) + - [Execution options](#execution-options) + - [Option 1 - Execute as a Tenant Member User](#option-1---execute-as-a-tenant-member-user) + - [Option 2 - Execute as a Tenant Guest User](#option-2---execute-as-a-tenant-guest-user) + - [Assign AAD Role - Directory readers](#assign-aad-role---directory-readers) + - [Option 3 - Execute as Service Principal](#option-3---execute-as-service-principal) + - [Grant API permissions](#grant-api-permissions) + - [Clone the Azure Governance Visualizer repository](#clone-the-azure-governance-visualizer-repository) + - [Run Azure Governance Visualizer from Console](#run-azure-governance-visualizer-from-console) + - [PowerShell \& Azure PowerShell modules](#powershell--azure-powershell-modules) + - [Connecting to Azure as User (Member or Guest)](#connecting-to-azure-as-user-member-or-guest) + - [Connecting to Azure using Service Principal](#connecting-to-azure-using-service-principal) + - [Run Azure Governance Visualizer](#run-azure-governance-visualizer) +- [Azure Governance Visualizer in Azure DevOps](#azure-governance-visualizer-in-azure-devops) + - [Create AzDO Project](#create-azdo-project) + - [Import Azure Governance Visualizer GitHub repository](#import-azure-governance-visualizer-github-repository) + - [Create AzDO Service Connection](#create-azdo-service-connection) + - [Create AzDO Service Connection - Option 1 - Create Service Connection´s Service Principal in the Azure Portal](#create-azdo-service-connection---option-1---create-service-connections-service-principal-in-the-azure-portal) + - [Azure Portal](#azure-portal) + - [Azure DevOps](#azure-devops) + - [Create AzDO Service Connection - Option 2 - Create Service Connection in AzDO](#create-azdo-service-connection---option-2---create-service-connection-in-azdo) + - [Grant permissions in Azure](#grant-permissions-in-azure-1) + - [Grant permissions in AAD](#grant-permissions-in-aad) + - [API permissions](#api-permissions) + - [Grant permissions on Azure Governance Visualizer AzDO repository](#grant-permissions-on-azure-governance-visualizer-azdo-repository) + - [OPTION 1 (legacy) - Edit AzDO YAML file (.pipelines folder)](#option-1-legacy---edit-azdo-yaml-file-pipelines-folder) + - [OPTION 1 (legacy) - Create AzDO Pipeline (.pipelines folder)](#option-1-legacy---create-azdo-pipeline-pipelines-folder) + - [OPTION 2 (new) - Edit AzDO Variables YAML file (.azuredevops folder)](#option-2-new---edit-azdo-variables-yaml-file-azuredevops-folder) + - [OPTION 2 (new) Create AzDO Pipeline (.azuredevops folder)](#option-2-new-create-azdo-pipeline-azuredevops-folder) + - [Run the AzDO Pipeline](#run-the-azdo-pipeline) + - [Create AzDO Wiki (WikiAsCode)](#create-azdo-wiki-wikiascode) +- [Azure Governance Visualizer in GitHub Actions](#azure-governance-visualizer-in-github-actions) + - [Create GitHub repository](#create-github-repository) + - [Import Code](#import-code) + - [Azure Governance Visualizer YAML](#azure-governance-visualizer-yaml) + - [Store the credentials in GitHub (Azure Governance Visualizer YAML)](#store-the-credentials-in-github-azure-governance-visualizer-yaml) + - [Workflow permissions](#workflow-permissions) + - [Edit the workflow YAML file (Azure Governance Visualizer YAML)](#edit-the-workflow-yaml-file-azure-governance-visualizer-yaml) + - [Run Azure Governance Visualizer in GitHub Actions (Azure Governance Visualizer YAML)](#run-azure-governance-visualizer-in-github-actions-azure-governance-visualizer-yaml) + - [Azure Governance Visualizer OIDC YAML](#azure-governance-visualizer-oidc-yaml) + - [Store the credentials in GitHub (Azure Governance Visualizer OIDC YAML)](#store-the-credentials-in-github-azure-governance-visualizer-oidc-yaml) + - [Workflow permissions](#workflow-permissions-1) + - [Edit the workflow YAML file (Azure Governance Visualizer OIDC YAML)](#edit-the-workflow-yaml-file-azure-governance-visualizer-oidc-yaml) + - [Run Azure Governance Visualizer in GitHub Actions (Azure Governance Visualizer OIDC YAML)](#run-azure-governance-visualizer-in-github-actions-azure-governance-visualizer-oidc-yaml) +- [Azure Governance Visualizer GitHub Codespaces](#azure-governance-visualizer-github-codespaces) +- [Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App](#optional-publishing-the-azure-governance-visualizer-html-to-a-azure-web-app) + - [Prerequisites](#prerequisites) + - [Azure DevOps](#azure-devops-1) + - [GitHub Actions](#github-actions) + + +# Azure Governance Visualizer from Console ## Grant permissions in Azure * Requirements * To assign roles, you must have '__Microsoft.Authorization/roleAssignments/write__' permissions on the target Management Group scope (such as the built-in RBAC Role '__User Access Administrator__' or '__Owner__') -Create a '__Reader__' RBAC Role assignment on the target Management Group scope for the identity that shall run AzGovViz +Create a '__Reader__' RBAC Role assignment on the target Management Group scope for the identity that shall run Azure Governance Visualizer * PowerShell ```powershell -$objectId = "" +$objectId = "" $role = "Reader" $managementGroupId = "" @@ -76,7 +89,7 @@ New-AzRoleAssignment ` ### Option 1 - Execute as a Tenant Member User -Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) +Proceed with step [__Clone the Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) ### Option 2 - Execute as a Tenant Guest User @@ -91,12 +104,12 @@ If the tenant is hardened (AAD External Identities / Guest user access = most re * Requirements * To assign roles, you must have '__Privileged Role Administrator__' or '__Global Administrator__' role assigned [Assign Azure AD roles to users](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal) -Assign the AAD Role '__Directory Reader__' for the Guest User that shall run AzGovViz (work with the Guest User´s display name) +Assign the AAD Role '__Directory Reader__' for the Guest User that shall run Azure Governance Visualizer (work with the Guest User´s display name) * Azure Portal * [Assign a role](https://docs.microsoft.com/en-us/azure/active-directory/roles/manage-roles-portal#assign-a-role) -Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) +Proceed with step [__Clone the Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) ### Option 3 - Execute as Service Principal @@ -128,12 +141,12 @@ Grant API permissions for the Service Principal´s Application Permissions in Azure Active Directory for App registration: ![alt text](img/aadpermissionsportal_4.jpg "Permissions in Azure Active Directory") -Proceed with step [__Clone the AzGovViz repository__](#clone-the-azgovviz-repository) +Proceed with step [__Clone the Azure Governance Visualizer repository__](#clone-the-azure-governance-visualizer-repository) -## Clone the AzGovViz repository +## Clone the Azure Governance Visualizer repository * Requirements - * To clone the AzGovViz GitHub repository you need to have GIT installed + * To clone the Azure Governance Visualizer GitHub repository you need to have GIT installed * Install Git: [https://git-scm.com/download/win](https://git-scm.com/download/win) * PowerShell @@ -143,9 +156,9 @@ Set-Location "c:\Git" git clone "https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git" ``` -Proceed with step [__Run AzGovViz from Console__](#run-azgovviz-from-console) +Proceed with step [__Run Azure Governance Visualizer from Console__](#run-azure-governance-visualizer-from-console) -## Run AzGovViz from Console +## Run Azure Governance Visualizer from Console ### PowerShell & Azure PowerShell modules @@ -181,9 +194,9 @@ Connect-AzAccount -ServicePrincipal -TenantId -Credential $pscredenti User: Enter '__Application (client) ID__' of the App registration OR '__Application ID__' of the Service Principal (Enterprise Application) Password for user \: Enter App registration´s secret -### Run AzGovViz +### Run Azure Governance Visualizer -Familiarize yourself with the available [parameters](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting#usage) for AzGovViz +Familiarize yourself with the available [parameters](https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting#usage) for Azure Governance Visualizer * PowerShell @@ -199,19 +212,19 @@ Note if not using the `-OutputPath` parameter, all outputs will be created in th c:\Git\Azure-MG-Sub-Governance-Reporting\pwsh\AzGovVizParallel.ps1 -ManagementGroupId -OutputPath "c:\AzGovViz-Output" ``` -# AzGovViz in Azure DevOps +# Azure Governance Visualizer in Azure DevOps ## Create AzDO Project [Create a project](https://docs.microsoft.com/en-us/azure/devops/organizations/projects/create-project?view=azure-devops&tabs=preview-page#create-a-project) -## Import AzGovViz GitHub repository +## Import Azure Governance Visualizer GitHub repository -AzGovViz Clone URL: `https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git` +Azure Governance Visualizer Clone URL: `https://github.com/JulianHayward/Azure-MG-Sub-Governance-Reporting.git` [Import into a new repo](https://docs.microsoft.com/en-us/azure/devops/repos/git/import-git-repository?view=azure-devops#import-into-a-new-repo) -Note: the AzGovViz GitHub repository is public - no authorization required +Note: the Azure Governance Visualizer GitHub repository is public - no authorization required ## Create AzDO Service Connection @@ -228,7 +241,7 @@ There are two options to create the Service Connection: * Navigate to 'Azure Active Directory' * Click on '__App registrations__' * Click on '__New registration__' -* Name your application (e.g. 'AzGovViz_SC') +* Name your application (e.g. 'AzureGovernanceVisualizer_SC') * Click '__Register__' * Your App registration has been created, in the '__Overview__' copy the '__Application (client) ID__' as we will need it later to setup the Service Connection in AzDO * Under '__Manage__' click on '__Certificates & Secrets__' @@ -237,9 +250,9 @@ There are two options to create the Service Connection: * A new client secret has been created, copy the secret´s value as we will need it later to setup the Service Connection in AzDO __Note:__ if you do not assign the RBAC 'Reader' role to the Management group at this stage then the '__Verify__' step in [Azure DevOps](#azure-devops) will fail. -* In the portal proceed to '__Management Groups__', select the scope at which AzGovViz will run, usually __Tenant Root Group__ +* In the portal proceed to '__Management Groups__', select the scope at which Azure Governance Visualizer will run, usually __Tenant Root Group__ * Go to '__Access Control (IAM)__', '__Grant Access__' and '__Add Role Assignment__', select '__Reader__', click '__Next__' -* Now '__Select Member__', this will be the name of the Application you created above (e.g. 'AzGovViz_SC'). +* Now '__Select Member__', this will be the name of the Application you created above (e.g. 'AzureGovernanceVisualizer_SC'). * Select '__Next__', '__Review + Assign__' #### Azure DevOps @@ -323,20 +336,20 @@ Grant API permissions for the Service Principal´s Application that we created e Permissions in Azure Active Directory for App registration: ![alt text](img/aadpermissionsportal_4.jpg "Permissions in Azure Active Directory") -## Grant permissions on AzGovViz AzDO repository +## Grant permissions on Azure Governance Visualizer AzDO repository -When the AzDO pipeline executes the AzGovViz script the outputs should be pushed back to the AzGovViz AzDO repository, in order to do this we need to grant the AzDO Project´s Build Service account with 'Contribute' permissions on the repository +When the AzDO pipeline executes the Azure Governance Visualizer script the outputs should be pushed back to the Azure Governance Visualizer AzDO repository, in order to do this we need to grant the AzDO Project´s Build Service account with 'Contribute' permissions on the repository -* Grant permissions on the AzGovViz AzDO repository +* Grant permissions on the Azure Governance Visualizer AzDO repository * In AzDO click on '__Project settings__' (located on the bottom left), under '__Repos__' open the '__Repositories__' page - * Click on the AzGovViz AzDO Repository and select the tab '__Security__' + * Click on the Azure Governance Visualizer AzDO Repository and select the tab '__Security__' * On the right side search for the Build Service account __%Project name% Build Service (%Organization name%)__ and grant it with '__Contribute__' permissions by selecting '__Allow__' (no save button available) ## OPTION 1 (legacy) - Edit AzDO YAML file (.pipelines folder) * Click on '__Repos__' -* Navigate to the AzGovViz Repository +* Navigate to the Azure Governance Visualizer Repository * In the folder '__pipeline__' click on '__AzGovViz.yml__' and click '__Edit__' * Under the variables section * Enter the Service Connection name that you copied earlier (ServiceConnection) @@ -348,7 +361,7 @@ When the AzDO pipeline executes the AzGovViz script the outputs should be pushed * Click on '__Pipelines__' * Click on '__New pipeline__' * Select '__Azure Repos Git__' -* Select the AzGovViz repository +* Select the Azure Governance Visualizer repository * Click on '__Existing Azure Pipelines YAML file__' * Under '__Path__' select '__/.pipelines/AzGovViz.yml__' (the YAML file we edited earlier) * Click ' __Save__' @@ -358,7 +371,7 @@ When the AzDO pipeline executes the AzGovViz script the outputs should be pushed >For the '__parameters__' and '__variables__' sections, details about each parameter or variable is documented inline. * Click on '__Repos__' -* Navigate to the AzGovViz repository +* Navigate to the Azure Governance Visualizer repository * In the folder '__/.azuredevops/pipelines__' click on '__AzGovViz.variables.yml__' and click '__Edit__' * If needed, modify the '__parameters__' section: * For more information about [parameters](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/runtime-parameters) @@ -375,7 +388,7 @@ When the AzDO pipeline executes the AzGovViz script the outputs should be pushed * Click on '__Pipelines__' * Click on '__New pipeline__' * Select '__Azure Repos Git__' -* Select the AzGovViz repository +* Select the Azure Governance Visualizer repository * Click on '__Existing Azure Pipelines YAML file__' * Under '__Path__' select '__/.azuredevops/pipelines/AzGovViz.pipeline.yml__' * Click ' __Save__' @@ -383,7 +396,7 @@ When the AzDO pipeline executes the AzGovViz script the outputs should be pushed ## Run the AzDO Pipeline * Click on '__Pipelines__' -* Select the AzGovViz pipeline +* Select the Azure Governance Visualizer pipeline * Click '__Run pipeline__' ## Create AzDO Wiki (WikiAsCode) @@ -393,12 +406,12 @@ Once the pipeline has executed successfully we can setup our Wiki (WikiAsCode) * Click on '__Overview__' * Click on '__Wiki__' * Click on '__Publish code as wiki__' -* Select the AzGovViz repository +* Select the Azure Governance Visualizer repository * Select the folder '__wiki__' and click '__OK__' * Enter a name for the Wiki * Click '__Publish__' -# AzGovViz in GitHub Actions +# Azure Governance Visualizer in GitHub Actions ## Create GitHub repository @@ -420,14 +433,14 @@ Use this workflow if you want to store your Application (App registration) secre 2. [AzGovViz_OIDC.yml](#azgovviz-oidc-yaml) Use this workflow if you want leverage the [OIDC (Open ID Connect) feature](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure) - no secret stored in GitHub -## AzGovViz YAML +## Azure Governance Visualizer YAML For the GitHub Actiom to authenticate and connect to Azure we need to create Service Principal (Application) In the Azure Portal navigate to 'Azure Active Directory' * Click on '__App registrations__' * Click on '__New registration__' -* Name your application (e.g. 'AzGovViz_SC') +* Name your application (e.g. 'AzureGovernanceVisualizer_SC') * Click '__Register__' * Your App registration has been created, in the '__Overview__' copy the '__Application (client) ID__' as we will need it later to setup the secrets in GitHub * Under '__Manage__' click on '__Certificates & Secrets__' @@ -435,7 +448,7 @@ In the Azure Portal navigate to 'Azure Active Directory' * Provide a good description and choose the expiry time based on your need and click '__Add__' * A new client secret has been created, copy the secret´s value as we will need it later to setup the secrets in GitHub -### Store the credentials in GitHub (AzGovViz YAML) +### Store the credentials in GitHub (Azure Governance Visualizer YAML) In GitHub navigate to 'Settings' * Click on 'Secrets' @@ -460,27 +473,27 @@ In GitHub navigate to 'Settings' * Under 'Workflow permissions' select '__Read and write permissions__' * Click 'Save' -### Edit the workflow YAML file (AzGovViz YAML) +### Edit the workflow YAML file (Azure Governance Visualizer YAML) * In the folder `./github/workflows` edit the YAML file `AzGovViz.yml` * In the `env` section enter you Management Group ID -* If you want to continuously run AzGovViz then enable the `schedule` in the `on` section +* If you want to continuously run Azure Governance Visualizer then enable the `schedule` in the `on` section -### Run AzGovViz in GitHub Actions (AzGovViz YAML) +### Run Azure Governance Visualizer in GitHub Actions (Azure Governance Visualizer YAML) In GitHub navigate to 'Actions' * Click 'Enable GitHub Actions on this repository' -* Select the AzGovViz workflow +* Select the Azure Governance Visualizer workflow * Click 'Run workflow' -## AzGovViz OIDC YAML +## Azure Governance Visualizer OIDC YAML For the GitHub Actiom to authenticate and connect to Azure we need to create Service Principal (Application). Using OIDC we will however not have the requirement to create a secret, nore store it in GitHub - awesome :) * Navigate to 'Azure Active Directory' * Click on '__App registrations__' * Click on '__New registration__' -* Name your application (e.g. 'AzGovViz_SC') +* Name your application (e.g. 'AzureGovernanceVisualizer_SC') * Click '__Register__' * Your App registration has been created, in the '__Overview__' copy the '__Application (client) ID__' as we will need it later to setup the secrets in GitHub * Under '__Manage__' click on '__Certificates & Secrets__' @@ -490,11 +503,11 @@ For the GitHub Actiom to authenticate and connect to Azure we need to create Ser * Fill the field 'Organization' with your GitHub Organization name * Fill the field 'Repository' with your GitHub repository name * For the entity type select 'Branch' -* Fill the field 'GitHub branch name' with your branch name (default is 'master' if you imported the AzGovViz repository) -* Fill the field 'Name' with a name (e.g. AzGovViz_GitHub_Actions) +* Fill the field 'GitHub branch name' with your branch name (default is 'master' if you imported the Azure Governance Visualizer repository) +* Fill the field 'Name' with a name (e.g. AzureGovernanceVisualizer_GitHub_Actions) * Click 'Add' -### Store the credentials in GitHub (AzGovViz OIDC YAML) +### Store the credentials in GitHub (Azure Governance Visualizer OIDC YAML) In GitHub navigate to 'Settings' * Click on 'Secrets' @@ -516,34 +529,34 @@ In GitHub navigate to 'Settings' * Under 'Workflow permissions' select '__Read and write permissions__' * Click 'Save' -### Edit the workflow YAML file (AzGovViz OIDC YAML) +### Edit the workflow YAML file (Azure Governance Visualizer OIDC YAML) * In the folder `./github/workflows` edit the YAML file `AzGovViz_OIDC.yml` * In the `env` section enter you Management Group ID -* If you want to continuously run AzGovViz then enable the `schedule` in the `on` section +* If you want to continuously run Azure Governance Visualizer then enable the `schedule` in the `on` section -### Run AzGovViz in GitHub Actions (AzGovViz OIDC YAML) +### Run Azure Governance Visualizer in GitHub Actions (Azure Governance Visualizer OIDC YAML) In GitHub navigate to 'Actions' * Click 'Enable GitHub Actions on this repository' * Select the AzGovViz_OIDC workflow * Click 'Run workflow' -# AzGovViz GitHub Codespaces +# Azure Governance Visualizer GitHub Codespaces Note: Codespaces is available for organizations using GitHub Team or GitHub Enterprise Cloud. [Quickstart for Codespaces](https://docs.github.com/en/codespaces/getting-started/quickstart) -![alt text](img/codespaces0.png "AzGovViz GitHub Codespaces") +![alt text](img/codespaces0.png "Azure Governance Visualizer GitHub Codespaces") -![alt text](img/codespaces1.png "AzGovViz GitHub Codespaces") +![alt text](img/codespaces1.png "Azure Governance Visualizer GitHub Codespaces") -![alt text](img/codespaces2.png "AzGovViz GitHub Codespaces") +![alt text](img/codespaces2.png "Azure Governance Visualizer GitHub Codespaces") -![alt text](img/codespaces3.png "AzGovViz GitHub Codespaces") +![alt text](img/codespaces3.png "Azure Governance Visualizer GitHub Codespaces") -![alt text](img/codespaces4.png "AzGovViz GitHub Codespaces") +![alt text](img/codespaces4.png "Azure Governance Visualizer GitHub Codespaces") -# Optional Publishing the AzGovViz HTML to a Azure Web App +# Optional Publishing the Azure Governance Visualizer HTML to a Azure Web App There are instances where you may want to publish the HTML output to a webapp so that anybody in the business can see up to date status of the Azure governance. diff --git a/version.txt b/version.txt index a8f90957..98d02ad4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v6_major_20230302_1 \ No newline at end of file +v6_major_20230306_1 \ No newline at end of file